Search code examples
node.jssinonsinon-chai

How can I test that a promise have been waited for (and not just created) using Sinon?


Let's say I have a function:

const someAction = async(): Promise<string> => {
    /* do stuff */
};

And I have some code that just needs to run this action, ignoring the result. But I have a bug - I don't want for action to complete:

someAction();

Which, instead, should've been looked like this:

await someAction();

Now, I can check that this action was ran:

const actionStub = sinon.stub(someAction);
expect(actionStub).to.have.been.calledWith();

But what's the most concise way to check that this promise have been waited on?

I understand how to implement this myself, but I suspect it must have already been implemented in sinon or sinon-chai, I just can't find anything.


Solution

  • I can certainly say that nothing like this exists in sinon or sinon-chai.

    This is a difficulty inherent to testing any promise-based function where the result isn't used. If the result is used, you know the promise has to be resolved before proceeding with said result. If it is not, things get more complex and kind of outside of the scope of what sinon can do for you with a simple stub.

    A naive approach is to stub the action with a fake that sets some variable (local to your test) to track the status. Like so:

    let actionComplete = false;
    const actionStub = sinon.stub(someAction).callsFake(() => {
        return new Promise((resolve) => {
            setImmediate(() => {
                actionComplete = true;
                resolve();
            });
        });
    });
    
    expect(actionStub).to.have.been.calledWith();
    expect(actionComplete).to.be.true;
    

    Of course, the problem here is that awaiting any promise, not necessarily this particular one, will pass this test, since the variable will get set on the next step of the event loop, regardless of what caused you to wait for that next step.

    For example, you could make this pass with something like this in your code under test:

    someAction();
    await new Promise((resolve) => {
        setImmediate(() => resolve());
    });
    

    A more robust approach will be to create two separate tests. One where the promise resolves, and one where the promise rejects. You can test to make sure the rejection causes the containing function to reject with the same error, which would not be possible if that specific promise was not awaited.

    const actionStub = sinon.stub(someAction).resolves();
    
    // In one test
    expect(actionStub).to.have.been.calledWith();
    
    // In another test
    const actionError = new Error('omg bad error');
    actionStub.rejects(actionError);
    
    // Assuming your test framework supports returning promises from tests.
    return functionUnderTest()
        .then(() => {
            throw new Error('Promise should have rejected');
        }, (err) => {
            expect(err).to.equal(actionError);
        });
    
    

    Some assertion libraries and extensions (maybe chai-as-promised) may have a way of cleaning up that use of de-sugared promises there. I didn't want to assume too much about the tools you're using and just tried to make sure the idea gets across.