This bit from a very related answer (mine), shows why it is not really that surprising:
ES modules are not mutable by default, which means Sinon can't do zilch.
The EcmaScript spec dictates this, so the only current way to mutate the exports is for the runtime to not adhere to the spec. This is essentially what Jest does: it provides its own runtime, translates the import calls into equivalent CJS calls (require
) calls and provides its own require
implementation in that runtime that hooks into the loading process. The resulting "module" usually has mutable exports that you can overwrite (i.e. stub).
Jest does not support native (as in no transpilation/modification of source) ESM either. Track issues 4842 and 9430 for how complex this (requires changes to Node).
So, no, Sinon cannot do this on its own. It is only a stubbing library. It does not touch the runtime or do anything magic, as it must work regardless of environment.
Now back to your original issue: testing your module. The only way I see this happening is through some sort of dependency injection mechanism (which you touch upon in alternative C). You obviously have some (internal/external) state your module depends on, so that means you need a way to change that state from the outside or inject a test double (what you are trying).
One easy way is just to create a setter strictly meant for testing:
function callNetworkService(...args){
// do something slow or brittle
}
let _doTestForConditionA = callNetworkService;
export function __setDoTestForConditionA(fn){
_doTestForConditionA = fn;
}
export function __reset(){
_doTestForConditionA = callNetworkService;
}
export function testForConditionA(...args) {
return _doTestForConditionA(...args);
}
You would then test your module simply like this:
afterEach(() => {
example.__reset();
});
test('that my module calls the outside and return X', async () => {
const fake = sinon.fake.resolves({result: 42});
example.__setDoTestForConditionA(fake);
const pendingPromise = example.doSomething();
expect(fake.called).to.equal(true);
expect((await pendingPromise).result).toEqual(42);
});
Yes, you do modify your SUT to allow testing, but I have never found that all that offensive. The technique works regardless of framework (Jasmine, Mocha, Jest) or runtime (browser, Node, JVM) and reads fine.
Optionally injected dependencies
You do mention injecting the dependencies into the function actually depending on them, and that has some issues that would propagate all over the codebase.
I would like to challenge that a bit by showing a technique I have used a bit in the past. See this comment (by me) on the Sinon issue tracker: https://github.com/sinonjs/sinon/issues/831#issuecomment-198081263
I use this example to show how you can inject stubs in a constructor that none of the usual consumers of this constructor needs to care about. Does require that you use some kind of Object
to not add additional parameters, of course.
/**
* Request proxy to intercept and cache outgoing http requests
*
* @param {Number} opts.maxAgeInSeconds how long a cached response should be valid before being refreshed
* @param {Number} opts.maxStaleInSeconds how long we are willing to use a stale cache in case of failing service requests
* @param {boolean} opts.useInMemCache default is false
* @param {Object} opts.stubs for dependency injection in unit tests
* @constructor
*/
function RequestCacher (opts) {
opts = opts || {};
this.maxAge = opts.maxAgeInSeconds || 60 * 60;
this.maxStale = opts.maxStaleInSeconds || 0;
this.useInMemCache = !!opts.useInMemCache;
this.useBasicToken = !!opts.useBasicToken;
this.useBearerToken = !!opts.useBearerToken;
if (!opts.stubs) {
opts.stubs = {};
}
this._redisCache = opts.stubs.redisCache || require('./redis-cache');
this._externalRequest = opts.stubs.externalRequest || require('../request-helpers/external-request-handler');
this._memCache = opts.stubs.memCache || SimpleMemCache.getSharedInstance();
}
(see the issue tracker for expanded comments)
There is nothing forcing anyone to provide stubs, but a test can provide them to override how the dependencies work.