Search code examples
javascriptnode.jsasynchronoussinon

How to test async function with Sinon JS?


Below is a minimal example of what I want to achieve: I want to test fn3() being called if the async function fn2() resolved (when calling fn1). But I somehow failed to do that with sinon's stub syntax. I would like to know what I misunderstood.

// System Under Test

export function fn1() {
    fn2().then(() => {
        fn3();
    });
}

export async function fn2() {
    return new Promise((resolve, reject) => {
        // expensive work
        resolve();
    });
}

export function fn3() {
    console.log("fn3");
}

// Test

import * as Page from "xxx";

it("test async", () => {
    // stub this async to isolate the SUT
    sinon.stub(Page, "fn2").resolves();
    const stub = sinon.stub(Page, "fn3");

    Page.fn1();

    sinon.assert.calledOnce(stub);
});


/*

    AssertError: expected fn3 to be called once but was called 0 times

      276 |         Page.fn1();
      277 |
    > 278 |         sinon.assert.calledOnce(stub);
          |                      ^
      279 |     });
      280 | });

      at Object.fail (node_modules/sinon/lib/sinon/assert.js:107:21)
      at failAssertion (node_modules/sinon/lib/sinon/assert.js:66:16)
      at Object.calledOnce (node_modules/sinon/lib/sinon/assert.js:92:13)
      at Object.<anonymous> (src/test.test.tsx:278:22)
*/

Solution

  • As @jonrsharpe suggested, your sinon.assert.calledOnce(stub); will be called before the fn2 promise inside Page.fn1 is resolved (i.e. .then(() => { fn3(); }) is called), based of the JavaScript "single thread, event loop" nature. Here's an article for reference: https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke.

    You can try changing your code to:

        ...
    
        return Page.fn1().then(() => {
          sinon.assert.calledOnce(stub);
        });
        
        ...
    

    or

    it("test async", async () => {
        // stub this async to isolate the SUT
        sinon.stub(Page, "fn2").resolves();
        const stub = sinon.stub(Page, "fn3");
    
        await Page.fn1();
    
        sinon.assert.calledOnce(stub);
    });
    

    so that sinon.assert.calledOnce(stub); will be called after Page.fn1() is resolved.