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

324 Views Asked by At

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?

1

There are 1 best solutions below

0
Brian Adams On BEST ANSWER

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();