Search code examples
javascriptpromiseasync-awaitjestjsassertions

How to assert that a dependency was called using `async` in a Jest test?


I have a service that can decorate an async function with a configuration-toggled alternate behavior:

// decorator.js
const config = require('./config');
const logger = require('./logger');

function addAlternateBehavior(originalAsyncFunction, alternateAsyncFunction) {
    return async () => {
        if (config.useAlternateBehavior) {
            await alternateAsyncFunction();
        } else {
            await originalAsyncFunction();
        }
        logger.info('Behavior finished executing');
    };
}

exports.addAlternateBehavior = addAlternateBehavior;

I have a Jest unit test that verifies that the alternate behavior gets called when configured accordingly:

// decorator.test.js
const decorator = require('./decorator');
const config = require('./config');

it('returned function should use alternate behavior when configured to do so', async () => {
    // Arrange
    const originalAsyncFunction = jest.fn();
    const alternateAsyncFunction = jest.fn();
    config.useAlternateBehavior = true;

    // Act
    const decoratedFunction = decorator
        .addAlternateBehavior(originalAsyncFunction, alternateAsyncFunction);
    await decoratedFunction();

    // Assert
    expect(originalAsyncFunction.mock.calls.length).toBe(0);
    expect(alternateAsyncFunction.mock.calls.length).toBe(1);
});

I want to assert that when you call the decorated function with await, it also awaits the expected behavior. However, in the decorator, if I change await alternateAsyncFunction(); to just alternateAsyncFunction(), my unit test still passes.

How can I assert, in a unit test, that the function decorated by addAlternateBehavior() awaits the alternateAsyncFunction or originalAsyncFunction?


Solution

  • Give your async functions an implementation that waits at least two event loop cycles before calling an inner mock. Then test if that inner mock was called:

    const decorator = require('./decorator');
    const config = require('./config');
    
    it('returned function should use alternate behavior when configured to do so', async () => {
        // Arrange
        const originalInner = jest.fn();
        const originalAsyncFunction = jest.fn(async () => {
          await Promise.resolve();
          await Promise.resolve();
          originalInner();
        });
        const alternateInner = jest.fn();
        const alternateAsyncFunction = jest.fn(async () => {
          await Promise.resolve();
          await Promise.resolve();
          alternateInner();
        });
        config.useAlternateBehavior = true;
    
        // Act
        const decoratedFunction = decorator
            .addAlternateBehavior(originalAsyncFunction, alternateAsyncFunction);
        await decoratedFunction();
    
        // Assert
        expect(originalAsyncFunction.mock.calls.length).toBe(0);
        expect(originalInner.mock.calls.length).toBe(0);
        expect(alternateAsyncFunction.mock.calls.length).toBe(1);
        expect(alternateInner.mock.calls.length).toBe(1);
    });
    

    If the function created by addAlternateBehavior() doesn't await then the inner mock won't get called.

    Note that two await Promise.resolve(); statements are necessary since the first one resolves during the event loop cycle that runs during await decoratedFunction();