Test asynchronous code using Jest
Test code that contains method setTimeout
Let’s say we have some function that called another function after N seconds:
1
2
3
4
5
| function completeTask(task: Task) {
setTimeout(() => {
task.complete();
}, DELAY);
}
|
Okay, now we want to test that method task.complete was called when we called function completeTask:
1
2
3
4
5
6
7
8
9
10
11
12
13
| const { completeTask } = require("./completeTask");
describe("completeTask", () => {
const FAKE_TASK = {
complete: jest.fn(),
};
it("should complete task", () => {
completeTask(FAKE_TASK);
expect(FAKE_TASK.complete).toHaveBeenCalled();
});
});
|
The test will fail.
When test is failed JestJS says:
1
| A function to advance timers was called but the timers API is not mocked with fake timers. Call `jest.useFakeTimers()` in this test or enable fake timers globally by setting `"timers": "fake"` in the configuration file. This warning is likely a result of a default configuration change in Jest 15
|
So, we updated JestJS configuration file or added jest.useFakeTimers() in test’s file but test is still failing.
Why? Because the line with expect(FAKE_TASK.complete).toHaveBeenCalled();
is called AFTER executing code inside setTimeout (after calling task.complete).
We need to call method jest.advanceTimersByTime(DELAY):
1
2
3
4
5
6
7
| it("should complete task", () => {
completeTask(FAKE_TASK);
jest.advanceTimersByTime(2500);
expect(FAKE_TASK.complete).toHaveBeenCalled();
});
|
Basically, it makes that test (line expect) is executing like if DELAY time (in our case is 2500) is passed.
Code with Promise
Function that has Promise inside:
1
2
3
4
5
6
7
| function completeTask(task: Task, onComplete: CompleteCallback) {
setTimeout(() => {
task.complete().then(() => {
onComplete();
});
}, DELAY);
}
|
We want to test that callback function onComplete has been called:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| describe("completeTask", () => {
const FAKE_TASK = {
complete: jest.fn().mockResolvedValue(null),
};
const handleComplete = jest.fn();
it("should complete task", async () => {
completeTask(FAKE_TASK, handleComplete);
jest.advanceTimersByTime(2500);
expect(FAKE_TASK.complete).toHaveBeenCalled();
expect(handleComplete).toHaveBeenCalled();
});
});
|
The test will fail because handleComplete has never been called
It is because of Promise inside completeTask. We can fix that by dirty hack with async / await:
1
2
3
4
5
6
7
8
9
10
| it("should complete task", async () => {
completeTask(FAKE_TASK, handleComplete);
jest.advanceTimersByTime(2500);
expect(FAKE_TASK.complete).toHaveBeenCalled();
await Promise.resolve(); // you can also write "await undefined". but why?...
expect(handleComplete).toHaveBeenCalled();
});
|
Don’t do that. If you need to do this hack it means you’re doing something wrong. It’s just demonstration that this hack exists.