javascriptnode.jsmockingsinoncommonjs

Is it possible to stub an exported function in a CommonJS module using sinon?


In almost 2021, is there a way to mock a single function? I mean function without object.

// demo.js
module.exports = () => {
  return 'real func demo';
};

// demo.test.js
const demo = require("./demo");
// ...
sinon.stub(demo).callsFake(() => {
  return 'mocked function';
});
expect(demo()).to.eq('mocked function')


Solution

  • Addendum in 2024: I wrote a real-world stubbing guide at sinon.org that might be useful for those who are inquiring into how one could achieve dependency stubbing with ESM and/or TypeScript. I list 3 very different approaches, from tooling to pure dependency injection, that all achieve this goal.


    I'll just quote myself here:

    You cannot stub standalone exported functions in an ES2015 compliant module (ESM) nor a CommonJS module in Node. That is just how these module systems work. It's only after transforming them into something else you might be able to achieve what you want. How you replace modules is totally environment specific and is why Sinon touts itself as "Standalone test spies, stubs and mocks for JavaScript" and not a module replacement tool, as that is better left to environment specific utils (proxyquire, various webpack loaders, Jest, etc) for whatever env you are in.

    For a more deep-dive into why this will never work in CommonJS when using destructuring, see my answer here. You need to inject a module loader that intercepts the loading.

    This is what trying to use Sinon to stub out an ESM module looks like in a spec compliant environment:

    $ node esm-stubbing.mjs
    /private/tmp/test/node_modules/sinon/lib/sinon/stub.js:72
            throw new TypeError("ES Modules cannot be stubbed");
                  ^
    
    TypeError: ES Modules cannot be stubbed
        at Function.stub (/private/tmp/test/node_modules/sinon/lib/sinon/stub.js:72:15)
        at Sandbox.stub (/private/tmp/test/node_modules/sinon/lib/sinon/sandbox.js:388:37)
        at file:///private/tmp/test/esm-stubbing.mjs:4:7
    
    $ cat esm-stubbing.mjs
    import sinon from "sinon";
    import * as myModule from "./module.mjs";
    
    sinon.stub(myModule, "foo").returns(42);
    

    This is just Sinon itself throwing an error when it sees that the namespace of the module is read-only, instead of letting Node throw a more cryptic error message.