Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jest test promise resolution and event loop tick

I have a failing Jest test case for code where I'm using promises. It looks like the resolution of the promise is happening after the test has completed, meaning I can't check that my promise resolution code has been executed.

It feels like I need to make the event loop tick so the promise is resolved and the resolution code is executed, but haven't found anything that can do that in Jest.

Here's a sample case. The code to be tested:

const Client = require('SomeClient');

module.exports.init = () => {
  Client.load().then(() => {
    console.log('load resolved');

    setTimeout(() => {
      console.log('load setTimeout fired, retrying init');
      module.exports.init();
    }, 1000);
  });
};

Test code:

jest.useFakeTimers();

const mockLoad = jest.fn().mockImplementation(() => Promise.resolve());

jest.mock('SomeClient', () => {
  return {
    load: mockLoad
  };
}, { virtual: true });

const promiseTest = require('./PromiseTest');

describe('SomeClient Promise Test', () => {
  it('retries init after 10 secs', () => {
    promiseTest.init();

    expect(mockLoad).toHaveBeenCalledTimes(1);
    expect(setTimeout).toHaveBeenCalledTimes(1); // <-- FAILS - setTimeout has not been called

    jest.runAllTimers();

    expect(mockLoad).toHaveBeenCalledTimes(2);
  });
});

The expect(setTimeout).toHaveBeenCalledTimes(1); assertion fails (setTimeout has not been called at all), I think because the promise has not yet been resolved.

Am I doing something wrong here? Can I cause the event loop to tick inside the test?

like image 643
Andrew Borley Avatar asked Mar 21 '18 11:03

Andrew Borley


2 Answers

To tick the event loop inside your test, you should make it asynchronous. A nice workaround was suggested on GitHub.

Having flushPromises as suggested there

function flushPromises() {
  return new Promise(resolve => setImmediate(resolve));
}

your test will look like

describe('SomeClient Promise Test', () => {
  it('retries init after 10 secs', () => {
    promiseTest.init();
    expect(mockLoad).toHaveBeenCalledTimes(1);
    
    // notice return so jest knows that the test is asynchronous
    return flushPromises()
      .then(() => {
        expect(setTimeout).toHaveBeenCalledTimes(1);

        jest.runAllTimers();

        expect(mockLoad).toHaveBeenCalledTimes(2);
      });
   });
});

Or the same using async/await:

describe('SomeClient Promise Test', () => {
  it('retries init after 10 secs', async () => {
    promiseTest.init();
    expect(mockLoad).toHaveBeenCalledTimes(1);

    await flushPromises();
    expect(setTimeout).toHaveBeenCalledTimes(1);

    jest.runAllTimers();

    expect(mockLoad).toHaveBeenCalledTimes(2);
  });
});
like image 200
Sergey Lapin Avatar answered Nov 01 '22 10:11

Sergey Lapin


Not entirely sure why our React setup was different but Sergey's answer nearly got us there.

We needed these two bits in our test:

export function flushPromises(): Promise<void> {
  return new Promise(jest.requireActual("timers").setImmediate);
}
await flushPromises();
like image 1
Robert Rendell Avatar answered Nov 01 '22 10:11

Robert Rendell