ike
ike

Reputation: 127

Jest mock timers not working as expected asynchronously; how to make this test pass?

I am trying to test a queuing component that makes calls and handles a lot of scheduling. I want to test it with a mock api where the api responses are delayed as they would be in real life, but I want to use mock timers and fake the passage of time. In the following bare-bones example, the object under test is the Caller object.

function mockCall(): Promise<string> {
    return new Promise<string>(resolve => setTimeout(() => resolve("success"), 20));
}

const callReceiver = jest.fn((result: string) => { console.log(result)});

class Caller {
    constructor(call: () => Promise<string>,
                receiver: (result: string) => void) {
        call().then(receiver);
    }
}

it("advances mock timers correctly", () => {
   jest.useFakeTimers();
   new Caller(mockCall, callReceiver);
   jest.advanceTimersByTime(50);
   expect(callReceiver).toHaveBeenCalled();
});

I would think this test should pass, but instead the expect is evaluated before the timer is advanced, so the test fails. How can I write this test so it will pass?

By the way, this test does pass if I use real timers and delay the expect for more than 20 milliseconds, but I am specifically interested in using fake timers and advancing time with code, not waiting for real time to elapse.

Upvotes: 3

Views: 16909

Answers (2)

K3v1n 0X90
K3v1n 0X90

Reputation: 76

You can make the test work by returning the promise to jest as otherwise the execution of your test method is already finished and does not wait for the promise to be fulfilled.

function mockCall() {
  return new Promise(resolve => setTimeout(() => resolve('success'), 20));
}

const callReceiver = jest.fn((result) => { console.log(result); });

class Caller {

  constructor(callee, receiver) {
    this.callee = callee;
    this.receiver = receiver;
  }

  execute() {
    return this.callee().then(this.receiver);
  }
}

describe('my test suite', () => {
  it('advances mock timers correctly', () => {
    jest.useFakeTimers();
    const caller = new Caller(mockCall, callReceiver);
    const promise = caller.execute();
    jest.advanceTimersByTime(50);

    return promise.then(() => {
      expect(callReceiver).toHaveBeenCalled();
    });
  });
});

Upvotes: 2

skyboyer
skyboyer

Reputation: 23763

The reason is mockCall still returns Promise, even after you mocked timer. So call().then() will be executed as next microtask. To advance execution you can wrap your expect in microtask too:

it("advances mock timers correctly", () => {
  jest.useFakeTimers();
  new Caller(mockCall, callReceiver);
  jest.advanceTimersByTime(50);
  return Promise.resolve().then(() => {
    expect(callReceiver).toHaveBeenCalled()
  });
});

Beware of returning this Promise so jest would wait until it's done. To me using async/await it would look even better:

it("advances mock timers correctly", async () => {
   jest.useFakeTimers();
   new Caller(mockCall, callReceiver);
   jest.advanceTimersByTime(50);
   await Promise.resolve();
   expect(callReceiver).toHaveBeenCalled();
});

Btw the same thing each time you mock something that is returning Promise(e.g. fetch) - you will need to advance microtasks queue as well as you do with fake timers.

More on microtasks/macrotasks queue: https://abc.danch.me/microtasks-macrotasks-more-on-the-event-loop-881557d7af6f

Jest repo has open proposal on handling pending Promises in more clear way https://github.com/facebook/jest/issues/2157 but no ETA so far.

Upvotes: 8

Related Questions