Let's say I have the following function that periodically checks a condition until it is satisfied.
let loopWithSleep = (callback) => {
if (conds.every(cond => { return cond; })) {
console.log("wooho");
callback();
} else {
console.log("waiting...");
setTimeout(loopWithSleep, 500, callback);
}
};
And assume I have a test suite that looks like this:
describe('Callback', () => {
it('should NOT use the provided callback', (done) => {
var conds = [true, false];
loopWithSleep(done);
});
});
How can I test if a callback function (in the above example named done
) has not been called and yet pass the test?
The above example results in error:
Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test.
I was trying to wrap the test in chai.assert.throws( ... );
but with no success.
CodePudding user response:
First of all, the function design could use improvement. It relies on the global variable cond
, which is brittle; if cond
changes name elsewhere in the code, the function breaks. All data should be passed in as parameters. This helps isolate components and mitigate bugs and makes testing easier.
I'd suggest an initial rewrite like:
const callWhenConditionsMet = (callback, conds, ms=500) => {
if (conds.every(Boolean)) {
callback();
}
else {
setTimeout(callWhenConditionsMet, ms, callback, conds, ms);
}
};
This still seems suboptimal due to lack of promises or providing for arguments, not to mention using polling when an event-driven approach is probably more appropriate (a possible XY problem), but I'll assume it's good enough for your use case.
As for the test, you may be mistaking done()
for a mock/spy. done()
isn't appropriate for assertions; it's just there to indicate to Mocha that the test is finished when asynchronous callbacks are involved. It's essentially a shortcut to promisification. Having to use done
excessively may indicate poor design in your library or test suite (or both). Prefer promises.
Instead of done
, mock the callback with your favorite library such as Sinon, pass the mock into the function you're testing, step time forward as long as needed, then assert that the mock was (or wasn't) called.
Furthermore, use fake timers to avoid your tests taking a long time and to ensure reliability. If you're using fake timers, you don't need done
at all! (another XY problem)
const {assert} = require("chai");
const sinon = require("sinon");
const callWhenConditionsMet = (callback, conds, ms=500) => {
if (conds.every(Boolean)) {
callback();
}
else {
setTimeout(callWhenConditionsMet, ms, callback, conds, ms);
}
};
describe("callWhenConditionsMet", () => {
let clock;
let spy;
beforeEach(() => {
clock = sinon.useFakeTimers({toFake: ["setTimeout"]});
spy = sinon.spy();
});
afterEach(() => {
clock.restore();
spy = null;
});
it("should invoke the callback right away when all conditions are true", () => {
callWhenConditionsMet(spy, [true, true]);
assert.isTrue(spy.called);
});
it("should not invoke the callback when a condition is false", () => {
callWhenConditionsMet(spy, [true, false]);
clock.tick(2000);
assert.isFalse(spy.called);
});
it("should not initially call when false, but then call later when true", () => {
const conds = [false, true];
callWhenConditionsMet(spy, conds, 1000);
clock.tick(5000);
assert.isFalse(spy.called);
conds[0] = true;
clock.tick(2000);
assert.isTrue(spy.called);
});
});
CodePudding user response:
While the answer provided by @ggorlen is certainly useful I think it is way over engineered.
Below is my answered inspired by his comment to the question. It uses Mocha
and chai
with plugin chai-spies
.
describe("callWhenConditionsMet", () => {
beforeEach(() => {
conds = [true, false];
});
it('should eventually use the callback function', async function () {
const mock = chai.spy();
loopWithSleep(mock);
await new Promise(function(resolve) { setTimeout( function() {resolve(); }, 500);});
chai.expect( mock ).not.to.have.been.called();
conds[1]=true;
await new Promise(function(resolve) { setTimeout( function() {resolve(); }, 500);});
chai.expect( mock ).to.have.been.called();
});
});