Reputation: 199
I have code along the lines of:
// example.js
export function doSomething() {
if (!testForConditionA()) {
return;
}
performATask();
}
export function testForConditionA() {
// tests for something and returns true/false
// let's say this function hits a service or a database and can't be run in tests
...
}
export function performATask() {
...
}
// example.test.js
import * as example from 'example';
it('validates performATask() runs when testForConditionA() is true', () => {
const testForConditionAStub = sinon.stub(example, 'testForConditionA').returns(true);
const performATaskSpy = sinon.stub(example, 'performATask');
example.doSomething();
expect(performATaskSpy.called).to.equal(true);
});
(I know, this is a contrived example, but I tried to keep it short)
I haven't found a way to mock testForConditionA() using Sinon.
I know there are work arounds, like
A) place everything that's in example.js into a class, and then the functions of the class can be stubbed.
B) move testForConditionA() (and other dependencies) out of example.js into a new file, and then use proxyquire
C) inject the dependencies into doSomething()
However, none of these options are viable - I'm working in a large codebase, and many files would need a rewrite & overhaul. I've searched on this topic, and I see several other posts, like this Stubbing method in same file using Sinon, but outside of refactoring code into a separate class (or a factory as one person suggested), or refactoring into a separate file and using proxyquire, I haven't found a solution. I've used other testing & mocking libraries before in the past, so it's surprising that Sinon isn't able to do this. Or is it? Any suggestions on how to go about stubbing a function without refactoring the code it's trying to test?
Upvotes: 4
Views: 3358
Reputation: 22923
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.
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.
Upvotes: 2