diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 000000000..2b876984d --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,69 @@ +--- +layout: page +title: Frequently Asked Questions +--- + +# Frequently Asked Questions + +## Property Descriptor Errors + +### "Descriptor for property X is non-configurable and non-writable" + +If you encounter an error like this: + +``` +TypeError: Descriptor for property toBeMocked is non-configurable and non-writable +``` + +This error occurs when Sinon tries to stub or spy on a property that has been defined as immutable by JavaScript's property descriptor system. This is not a bug in Sinon, but rather a limitation imposed by the JavaScript engine itself. + +#### Common Causes + +1. **ES Module transpilation**: When ES modules are transpiled to CommonJS (e.g., by TypeScript, Babel, or SWC), the exported properties often become non-configurable and non-writable. + +2. **Object.freeze() or Object.seal()**: Objects that have been frozen or sealed have immutable properties. + +3. **Native browser/Node.js APIs**: Some built-in objects and their properties are inherently immutable. + +4. **Third-party libraries**: Some libraries define their exports with non-configurable descriptors. + +#### Solutions + +1. **Use dependency injection**: Instead of stubbing the import directly, pass the dependency as a parameter: + + ```javascript + // Instead of this: + import { toBeMocked } from "./module"; + sinon.stub(module, "toBeMocked"); // This might fail + + // Do this: + function myFunction(dependency = toBeMocked) { + return dependency(); + } + + // In tests: + const stub = sinon.stub(); + myFunction(stub); + ``` + +2. **Stub at the module level**: For ES modules, consider using a tool like `proxyquire` or `testdouble.js` for module-level mocking. + +3. **Use dynamic imports**: Dynamic imports can sometimes work around transpilation issues: + + ```javascript + // In your test + const module = await import("./module"); + sinon.stub(module, "toBeMocked"); + ``` + +4. **Restructure your code**: Consider whether the code under test can be refactored to be more testable. + +#### For TypeScript Users + +When using TypeScript with SWC or similar transpilers, see our [TypeScript with SWC guide]({% link _howto/typescript-swc.md %}) for specific solutions. + +#### Further Reading + +- [MDN: Object.getOwnPropertyDescriptor()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor) +- [MDN: Property descriptors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#Description) +- [How-to: Stub dependencies in CommonJS]({% link _howto/stub-dependency.md %}) diff --git a/lib/sinon/stub.js b/lib/sinon/stub.js index 8b9efc9b6..f4238d472 100644 --- a/lib/sinon/stub.js +++ b/lib/sinon/stub.js @@ -136,7 +136,9 @@ function assertValidPropertyDescriptor(descriptor, property) { } if (descriptor.isOwn && !descriptor.configurable && !descriptor.writable) { throw new TypeError( - `Descriptor for property ${property} is non-configurable and non-writable`, + `The descriptor for property \`${property}\` is non-configurable and non-writable. ` + + `Sinon cannot stub properties that are immutable. ` + + `See https://sinonjs.org/faq#property-descriptor-errors for help fixing this issue.`, ); } if ((descriptor.get || descriptor.set) && !descriptor.configurable) { diff --git a/test/issues/issues-test.js b/test/issues/issues-test.js index 3db0a496f..d73a8f2bf 100644 --- a/test/issues/issues-test.js +++ b/test/issues/issues-test.js @@ -780,7 +780,7 @@ describe("issues", function () { const instance = createInstanceFromClassWithReadOnlyPropertyDescriptor(); - // per #2491 this throws 'TypeError: Descriptor for property aMethod is non-configurable and non-writable' + // per #2491 this throws 'TypeError: The descriptor for property `aMethod` is non-configurable and non-writable. Sinon cannot stub properties that are immutable. See https://sinonjs.org/faq#property-descriptor-errors for help fixing this issue.' // that makes sense for descriptors taken from the object, but not its prototype, as we are free to change // the latter when setting it refute.exception(() => { diff --git a/test/shared-spy-stub-everything-tests.js b/test/shared-spy-stub-everything-tests.js index 07cbe0155..56759cb4b 100644 --- a/test/shared-spy-stub-everything-tests.js +++ b/test/shared-spy-stub-everything-tests.js @@ -159,9 +159,16 @@ module.exports = function shared(createSpyOrStub) { configurable: false, }); - assert.exception(function () { - createSpyOrStub(myObj, "ignoreme"); - }, new TypeError("Descriptor for property ignoreme is non-configurable and non-writable")); + assert.exception( + function () { + createSpyOrStub(myObj, "ignoreme"); + }, + new TypeError( + "The descriptor for property `ignoreme` is non-configurable and non-writable. " + + "Sinon cannot stub properties that are immutable. " + + "See https://sinonjs.org/faq#property-descriptor-errors for help fixing this issue.", + ), + ); }); it("throws on accessor property descriptors that are not configurable", function () {