Search code examples
javascriptasync-awaitjestjssettimeoutes6-promise

How to properly use Promises and Timers with Jest


I have searched both SO and Google and found a lot of similar questions and answers, but none of them seems to have helped me solve my issue.

I am attempting to write some test cases where I need to mock an async polling function. But no matter what I do I get:

Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Timeout

I set up some minimal test cases the reproduce the problem:

jest.useFakeTimers();

describe('timers test', () => {
  it('plain timer works as expected', () => {
    const mock = jest.fn();
    setTimeout(mock, 5000);

    jest.runAllTimers();
    expect(mock).toHaveBeenCalled();
  });

  it('Using a timer to mock a promise resolution results in a jest timeout error', async () => {
    const mock = jest.fn(() => {
      return new Promise((resolve) => setTimeout(resolve, 500));
    });

    const handler = jest.fn();

    await mock().then(handler);

    jest.runAllTimers();

    expect(handler).toHaveBeenCalled();
  });

  it('Using a timer to mock a promise rejection results in a jest timeout error', async () => {
    const mock = jest.fn(() => {
      return new Promise((resolve, reject) => setTimeout(reject, 500));
    });

    const handler = jest.fn();

    await mock().catch(handler);

    jest.runAllTimers();

    expect(handler).toHaveBeenCalled();
  });
});

Can someone explain what I am doing wrong and why?


Solution

  • So with a follow up comment from @Bergi, I relaized the done wasn't actually necessary either. I just needed to re-order some things. I then ran into a similar issue when testing chains of promises that further highlighted this so I added some cases for that.

    jest.useFakeTimers();
    
    describe('timers test', () => {
      it('Using a plain timer works as expected', () => {
        const mock = jest.fn();
        setTimeout(mock, 5000);
    
        jest.runAllTimers();
        expect(mock).toHaveBeenCalled();
      });
    
      it('Using a timer to mock a promise resolution', async () => {
        const mock = jest.fn(() => {
          return new Promise((resolve) => setTimeout(resolve, 500));
        });
    
        const handler = jest.fn();
    
        const actual = mock().then(handler);
        jest.runAllTimers();
        await actual;
    
        expect(handler).toHaveBeenCalled();
      });
    
      it('Using a timer to mock a promise rejection', async () => {
        const mock = jest.fn(() => {
          return new Promise((resolve, reject) => setTimeout(reject, 500));
        });
    
        const handler = jest.fn();
    
        const actual = mock().catch(handler);
        jest.runAllTimers();
        await actual;
    
        expect(handler).toHaveBeenCalled();
      });
    
      it('Using a timer to mock a promise resolve -> delay -> resolve chain', async () => {
        const mockA = jest.fn(() => {
          return Promise.resolve();
        });
    
        const mockB = jest.fn(() => {
          return new Promise((resolve, reject) => {
            setTimeout(resolve, 500);
          });
        });
    
        const handler = jest.fn();
    
        const actual = mockA()
          .then(() => {
            const mockProm = mockB();
            jest.runAllTimers();
            return mockProm;
          })
          .then(handler);
    
        jest.runAllTimers();
        await actual;
    
        expect(mockA).toHaveBeenCalled();
        expect(mockB).toHaveBeenCalled();
        expect(handler).toHaveBeenCalled();
      });
    
      it('Using a timer to mock a promise resolve -> delay -> reject chain', async () => {
        const mockA = jest.fn(() => {
          return Promise.resolve();
        });
    
        const mockB = jest.fn(() => {
          return new Promise((resolve, reject) => {
            setTimeout(reject, 500);
          });
        });
    
        const handler = jest.fn();
    
        const actual = mockA()
          .then(() => {
            const mockProm = mockB();
            jest.runAllTimers();
            return mockProm;
          })
          .catch(handler);
    
    
        await actual;
    
        expect(mockA).toHaveBeenCalled();
        expect(mockB).toHaveBeenCalled();
        expect(handler).toHaveBeenCalled();
      });
    });
    

    @Bergi' comment led me to the solution. I ended up making use of the done function, and removing the await. This seems to work at least in this minimal test case.

    jest.useFakeTimers();
    
    describe('timers test', () => {
      it('plain timer works as expected', () => {
        const mock = jest.fn();
        setTimeout(mock, 5000);
    
        jest.runAllTimers();
        expect(mock).toHaveBeenCalled();
      });
    
      it('Using a timer to mock a promise resolution results in a jest timeout error', async (done) => {
        const mock = jest.fn().mockImplementation(() => {
          return new Promise((resolve) => setTimeout(resolve, 500));
        });
    
        // make the handler invoke done to replace the await    
        const handler = jest.fn(done);
    
        mock().then(handler);
        jest.runAllTimers();
    
        expect(handler).toHaveBeenCalled();
      });
    
      it('Using a timer to mock a promise rejection results in a jest timeout error', async (done) => {
        const mock = jest.fn().mockImplementation(() => {
          return new Promise((resolve, reject) => setTimeout(reject, 500));
        });
    
        // make the handler invoke done to replace the await
        const handler = jest.fn(done);
    
        mock().catch(handler);
        jest.runAllTimers();
    
        expect(handler).toHaveBeenCalled();
      });
    });