0
votes

I am learning Jest and modern JavaScript, and am trying to test a piece of code. My tests are grouped within a describe() container, and I would like to reset mocks (and mock counts) between tests. There seem to be some questions on Stack Overflow that deal with resetting or clearing mock data, but they do not seem to work in my case, and I am too new to JS to understand what the differences might be.

Here is my SUT (in source.js):

module.exports = async function(delay) {

    let query;
    let ok;
    for (let i = 0; i < 5; i++) {
        query = context.functions.execute('getNextQuery');
        if (query) {
            ok = context.functions.execute('runQuery', query);
            if (ok) {
                context.functions.execute('markQueryAsRun', query.id);
            }
        } else {
            break;
        }

        // Be nice to the API
        await sleep(delay);
    }

    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
};

And here are the tests:

const findAndRunQueries = require('./source');

describe('Some tests for findAndRunQueries', () => {
    let globalMocks = {};

    // context.functions.execute() becomes a mock
    global.context = {
        functions: {
            execute: jest.fn((funcName, ...params) => {
                // This calls a mock that we set up per test
                return globalMocks[funcName](...params);
            })
        }
    };

    test('No queries need to be run', async () => {
        // Don't need to mock other funcs if this one is falsey
        setGlobalMock('getNextQuery', () => {
            return null;
        });

        await findAndRunQueries(0);

        // Ensure only getNextQuery is called
        expect(getNthMockFunctionCall(0)[0]).toBe('getNextQuery');
        expect(countMockFunctionCalls()).toBe(1);
    });

    test('One query needs to be run', async () => {
        let callCount = 0;

        // Return an object on the first run, null on the second
        setGlobalMock('getNextQuery', () => {
            if (callCount++ === 0) {
                return {
                    something: 'fixme'
                };
            } else {
                return null;
            }
        });

        setGlobalMock('runQuery', (query) => {
            return true;
        });

        setGlobalMock('markQueryAsRun', (queryId) => {
            // Does not need to return anything
        });

        await findAndRunQueries(0);

        // Ensure each func is called
        expect(getNthMockFunctionCall(0)[0]).toBe('getNextQuery');
        expect(getNthMockFunctionCall(1)[0]).toBe('runQuery');
        expect(getNthMockFunctionCall(2)[0]).toBe('markQueryAsRun');

        // We have a 4th call here, which returns null to terminate the loop early
        expect(getNthMockFunctionCall(3)[0]).toBe('getNextQuery');

        // Ensure there is no extra calls
        expect(countMockFunctionCalls()).toBe(4);
    });

    function setGlobalMock(funcName, func) {
        globalMocks[funcName] = func;
    }

    function getNthMockFunctionCall(n) {
        return global.context.functions.execute.mock.calls[n];
    }

    function countMockFunctionCalls() {
        return global.context.functions.execute.mock.calls.length;
    }
});

There are two tests here, No queries need to be run and One query needs to be run. The error is in the second one, and I suspect that what is happening is that the mock recordings are persisting from the first test, and spoiling the results.

I have confirmed this by running this test on its own:

node node_modules/jest/bin/jest.js -t 'One query needs to be run'

However if I run both tests, like so:

node node_modules/jest/bin/jest.js functions/findAndRunQueries

Then I get a failure:

  ● Some tests for findAndRunQueries › One query needs to be run

    expect(received).toBe(expected) // Object.is equality

    Expected: "runQuery"
    Received: "getNextQuery"

      53 |         // Ensure each func is called
      54 |         expect(getNthMockFunctionCall(0)[0]).toBe('getNextQuery');
    > 55 |         expect(getNthMockFunctionCall(1)[0]).toBe('runQuery');
         |                                              ^
      56 |         expect(getNthMockFunctionCall(2)[0]).toBe('markQueryAsRun');
      57 | 
      58 |         // We have a 4th call here, which returns null to terminate the loop early

      at Object.test (functions/findAndRunQueries/findAndRunQueries.test.js:55:46)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        0.637s, estimated 1s
Ran all test suites matching /functions\/findAndRunQueries/i.

It says that the first parameter in the second call in the second test (getNthMockFunctionCall(1)[0]) is getNextQuery, when I am expecting runQuery. I think this indicates that the mock results from the first test are persisting.

I have tried adding this:

    beforeAll(() => {
        jest.clearAllMocks();
    });

I believe that "clearing" and "resetting" in Jest parlance are different - clearing just resets the counts, resetting removes mock implementations also. So it is clearing that I want, but unfortunately this makes no difference.

What further things can I try?

1
beforeAll -> beforeEach?jonrsharpe
@jonrsharpe: you are absolutely right - thank you. Just in the last few minutes, I looked at beforeAll, and wondered: "Is that run for every test?" :=)halfer

1 Answers

2
votes

You could try adding:

afterEach(() => {
    jest.restoreAllMocks()
});

Also it is good practice to use beforeEach to define your test variables/mocks that you reuse so that they are defined separately for each test.