How can I assert a function has `await`ed an async function using should.js

1.7k Views Asked by At

I have an async function f that calls another async function g. To test if f calls g, I'm stubbing g using sinon and assert it's been called using should.js.

'use strict';

require('should-sinon');
const sinon = require('sinon');

class X {
    async f(n) {
        await this.g(n);
        // this.g(n); // I forget to insert `await`!
    }
    async g(n) {
        // Do something asynchronously
    }
}

describe('f', () => {
    it('should call g', async () => {
        const x = new X();
        sinon.stub(x, 'g').resolves();
        await x.f(10);
        x.g.should.be.calledWith(10);
    });
});

But this test passes even when I forget to use await when calling g in f.

One of the ways to catch this error is to make the stub return a dummy promise and check if its then is called.

it('should call g', async () => {
    const x = new X();
    const dummyPromise = {
        then: sinon.stub().yields()
    };
    sinon.stub(x, 'g').returns(dummyPromise);
    await x.f(10);
    x.g.should.be.calledWith(10);
    dummyPromise.then.should.be.called();
});

But this is a bit bothersome. Are there any convenient ways to do this?

2

There are 2 best solutions below

3
B M On

Your example for f shows flawed code design which becomes more obvious if you write the same function without async/await syntax:

f(n) { return g(n).then(()=>{}); }

This achieves the same behavior - whether g resolved becomes hard to tell (assuming you don't know if f returned g's promise, which is the same as not knowing whether f awaited g). If f is not interested in the result of g it should just simply return it, not hide it. Then you can simply test for the result.

If your point is that f might have to trigger several async calls sequentially awaiting several g_1, g_2,... to resolve, then you can build a test chain by asserting in the stub of g_n+1 that the dummy-promise of g_n has been resolved. In general your approach to test a dummy-promise for its status is fine.

0
sripberger On

Instead of stubbing then, you're best off stubbing g in such a way that it sets some boolean on the next event loop iteration. Then, you can check this boolean after calling f to make sure f waited for it:

it('should call g', async () => {
    const x = new X();
    let gFinished = false;
    sinon.stub(x, 'g').callsFake(() => {
        return new Promise((resolve) => {
            setImmediate(() => {
                gFinished = true;
                resolve();
            });
        });
    });
    await x.f(10);
    x.g.should.be.calledWith(10);
    gFinished.should.be.true();
});

Edit: Of course, this isn't a perfect guarantee because you could have f wait on any promise that waits at least as long as it takes for g to resolve. Like so:

async f(n) {
    this.g(n);
    await new Promise((resolve) => {
        setImmediate(() => {
            resolve();
        });
    });
}

This would cause the test I wrote to pass, even though it's still incorrect. So really it comes down to how strict you're trying to be with your tests. Do you want it to be literally impossible to have a false positive? Or is it ok if some obvious trickery can potentially throw it off?

In most cases I find that the latter is ok, but really that's up to you and/or your team.