Skip to content

Commit

Permalink
fixup! docs: update Timer Mocks guide for modern timers
Browse files Browse the repository at this point in the history
  • Loading branch information
sigveio committed Aug 16, 2021
1 parent 24ed2a4 commit aebeabf
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 130 deletions.
89 changes: 24 additions & 65 deletions docs/TimerMocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,13 @@ id: timer-mocks
title: Timer Mocks
---

The native timer functions (i.e., `setTimeout()`, `setInterval()`,
`clearTimeout()`, `clearInterval()`) are less than ideal for a testing
environment since they depend on real time to elapse. Jest can swap out timers
with functions that allow you to control the passage of time.
[Great Scott!][back-to-the-future-reference]
The native timer functions (i.e., `setTimeout()`, `setInterval()`, `clearTimeout()`, `clearInterval()`) are less than ideal for a testing environment since they depend on real time to elapse. Jest can swap out timers with functions that allow you to control the passage of time. [Great Scott!][back-to-the-future-reference]

[back-to-the-future-reference]: https://www.youtube.com/watch?v=QZoJ2Pt27BY

:::note

The default timer implementation changed in Jest 27 and is now based on
[@sinonjs/fake-timers][fake-timers-github].
The default timer implementation changed in Jest 27 and is now based on [@sinonjs/fake-timers][fake-timers-github].

:::

Expand All @@ -26,6 +21,7 @@ The default timer implementation changed in Jest 27 and is now based on
function timerTest(callback) {
setTimeout(() => callback('Timer finished!'), 10000);
}

module.exports = timerTest;
```

Expand Down Expand Up @@ -55,36 +51,23 @@ test('should invoke callback after timer ends', () => {
});
```

Here we call `jest.useFakeTimers()` to replace `setTimeout()` and other native
timer functions with mock functions. And since we use `jest.runAllTimers()` to
fast forward in time, the callback has a chance to run before the test
completes.
Here we call `jest.useFakeTimers()` to replace `setTimeout()` and other native timer functions with mock functions. And since we use `jest.runAllTimers()` to fast forward in time, the callback has a chance to run before the test completes.

## You are in the driver's seat

Fake timers will not automatically increment with the system clock. When they
are activated, time is essentially frozen until you say otherwise (or until
Jest times out).
Fake timers will not automatically increment with the system clock. When they are activated, time is essentially frozen until you say otherwise (or until Jest times out).

In the previous example, we use `jest.runAllTimers()` to fast forward and run
all tasks queued by the mocked timer functions. Without telling Jest to advance
time like this, our callback would never be invoked.
In the previous example, we use `jest.runAllTimers()` to fast forward and run all tasks queued by the mocked timer functions. Without telling Jest to advance time like this, our callback would never be invoked.

You can also have more fine-grained control with for example
`jest.advanceTimersByTime()` or `jest.advanceTimersToNextTimer()`. See the
[API documentation](/docs/jest-object#mock-timers) for more information.
You can also have more fine-grained control with for example `jest.advanceTimersByTime()` or `jest.advanceTimersToNextTimer()`. See the [API documentation](/docs/jest-object#mock-timers) for more information.

## Faking timers is a global operation

Calling `jest.useRealTimers()` will turn on fake timers for all tests within
the same file, until normal timers are restored with `jest.useRealTimers()`.
Calling `jest.useRealTimers()` will turn on fake timers for all tests within the same file, until normal timers are restored with `jest.useRealTimers()`.

The fake timers also have a global state that only resets each time you call
`jest.useFakeTimers()`.
The fake timers also have a global state that only resets each time you call `jest.useFakeTimers()`.

So while `jest.useFakeTimers()` and `jest.useRealTimers()` can be called from
anywhere (top-level, inside a `test` block, etc.), you need to be careful
in order to avoid unexpected behavior.
So while `jest.useFakeTimers()` and `jest.useRealTimers()` can be called from anywhere (top-level, inside a `test` block, etc.), you need to be careful in order to avoid unexpected behavior.

```js
test('do something with fake timers', () => {
Expand All @@ -97,10 +80,7 @@ test('do something with real timers (?)', () => {
});
```

In this example, since we never call `jest.useRealTimers()`, both tests
would end up using fake timers when run synchronously. And the state
(time/internal counters) of the fake timers would leak across since we did not
reset it.
In this example, since we never call `jest.useRealTimers()`, both tests would end up using fake timers when run synchronously. And the state (time/internal counters) of the fake timers would leak across since we did not reset it.

A better approach could be:

Expand All @@ -125,30 +105,20 @@ test('do something with real timers', () => {
});
```

Here we use an `afterEach` to call `jest.useRealTimers()` after every single
test.
Here we use an `afterEach` to call `jest.useRealTimers()` after every single test.

This pattern can help keep behavior predictable as your test file
grows.
This pattern can help keep behavior predictable as your test file grows.

- It will establish a clear baseline ->
"timers are real unless otherwise is set".
- The state of the fake timers is always reset between tests, since all tests
wanting to use them have to call `jest.useFakeTimers()` first.
- It will establish a clear baseline -> "timers are real unless otherwise is set".
- The state of the fake timers is always reset between tests, since all tests wanting to use them have to call `jest.useFakeTimers()` first.

## Handling recursive timers

Scenarios exist where you might have a recursive timer -- for example a function
that sets a timer to call the same function again.
Scenarios exist where you might have a recursive timer -- for example a function that sets a timer to call the same function again.

If you use `jest.runAllTimers()` to advance time, it will execute all pending
tasks. If those tasks themselves schedule new tasks, those will be continually
exhausted until there are no more tasks remaining in the queue. So with a
recursive timer, you could end up with an infinite loop.
If you use `jest.runAllTimers()` to advance time, it will execute all pending tasks. If those tasks themselves schedule new tasks, those will be continually exhausted until there are no more tasks remaining in the queue. So with a recursive timer, you could end up with an infinite loop.

To solve this, Jest ships with an alternative called
`jest.runOnlyPendingTimers()`. This will run only the tasks queued by
`setTimeout()` or `setInterval()` up until that point.
To solve this, Jest ships with an alternative called `jest.runOnlyPendingTimers()`. This will run only the tasks queued by `setTimeout()` or `setInterval()` up until that point.

```js title="/examples/timer/modern/infiniteTimerTest.js"
function infiniteTimerTest(callback) {
Expand Down Expand Up @@ -177,8 +147,8 @@ test('should not start recursive timer loop', () => {

infiniteTimerTest(callback);

// At this point only the timer in infiniteTimerTest() is scheduled,
// and time is frozen. So infiniteTimer() or the callback passed to it
// At this point only the timer in infiniteTimerTest() is scheduled,
// and time is frozen. So infiniteTimer() or the callback passed to it
// should not have been invoked.
expect(callback).not.toHaveBeenCalled();

Expand Down Expand Up @@ -211,21 +181,10 @@ TODO: Info about / link to more code examples here.

### Timer functions are no longer a mock or spy function. How can I assert against them?

In the legacy fake timers, prior to Jest 27, native timer functions were
replaced by Jest mock functions. After moving to a new implementation based on
an [external library][fake-timers-github], this is no longer a feature. So the
short answer is that you can't... at least not directly.
In the legacy fake timers, prior to Jest 27, native timer functions were replaced by Jest mock functions. After moving to a new implementation based on an [external library][fake-timers-github], this is no longer a feature. So the short answer is that you can't... at least not directly.

The specific timer function used should be considered an implementation detail,
and it would generally be more robust to test against what you expect to happen
when a task runs.
The specific timer function used should be considered an implementation detail, and it would generally be more robust to test against what you expect to happen when a task runs.

Looking back at the first code example on this page, you will see that we
assert if the mocked callback function is called with the parameter we expect.
This decouples us from the details of what happens inside `timerTest()`. At
some point in the future, we can safely replace `setTimeout()` with another
timer without invalidating the test.
Looking back at the first code example on this page, you will see that we assert if the mocked callback function is called with the parameter we expect. This decouples us from the details of what happens inside `timerTest()`. At some point in the future, we can safely replace `setTimeout()` with another timer without invalidating the test.

If you need to verify that callbacks are scheduled with a particular time or
interval, consider using `jest.advanceTimersByTime()` and make assertions based
on what you expect at different points in time.
If you need to verify that callbacks are scheduled with a particular time or interval, consider using `jest.advanceTimersByTime()` and make assertions based on what you expect at different points in time.
89 changes: 24 additions & 65 deletions website/versioned_docs/version-27.0/TimerMocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,13 @@ id: timer-mocks
title: Timer Mocks
---

The native timer functions (i.e., `setTimeout()`, `setInterval()`,
`clearTimeout()`, `clearInterval()`) are less than ideal for a testing
environment since they depend on real time to elapse. Jest can swap out timers
with functions that allow you to control the passage of time.
[Great Scott!][back-to-the-future-reference]
The native timer functions (i.e., `setTimeout()`, `setInterval()`, `clearTimeout()`, `clearInterval()`) are less than ideal for a testing environment since they depend on real time to elapse. Jest can swap out timers with functions that allow you to control the passage of time. [Great Scott!][back-to-the-future-reference]

[back-to-the-future-reference]: https://www.youtube.com/watch?v=QZoJ2Pt27BY

:::note

The default timer implementation changed in Jest 27 and is now based on
[@sinonjs/fake-timers][fake-timers-github].
The default timer implementation changed in Jest 27 and is now based on [@sinonjs/fake-timers][fake-timers-github].

:::

Expand All @@ -26,6 +21,7 @@ The default timer implementation changed in Jest 27 and is now based on
function timerTest(callback) {
setTimeout(() => callback('Timer finished!'), 10000);
}

module.exports = timerTest;
```

Expand Down Expand Up @@ -55,36 +51,23 @@ test('should invoke callback after timer ends', () => {
});
```

Here we call `jest.useFakeTimers()` to replace `setTimeout()` and other native
timer functions with mock functions. And since we use `jest.runAllTimers()` to
fast forward in time, the callback has a chance to run before the test
completes.
Here we call `jest.useFakeTimers()` to replace `setTimeout()` and other native timer functions with mock functions. And since we use `jest.runAllTimers()` to fast forward in time, the callback has a chance to run before the test completes.

## You are in the driver's seat

Fake timers will not automatically increment with the system clock. When they
are activated, time is essentially frozen until you say otherwise (or until
Jest times out).
Fake timers will not automatically increment with the system clock. When they are activated, time is essentially frozen until you say otherwise (or until Jest times out).

In the previous example, we use `jest.runAllTimers()` to fast forward and run
all tasks queued by the mocked timer functions. Without telling Jest to advance
time like this, our callback would never be invoked.
In the previous example, we use `jest.runAllTimers()` to fast forward and run all tasks queued by the mocked timer functions. Without telling Jest to advance time like this, our callback would never be invoked.

You can also have more fine-grained control with for example
`jest.advanceTimersByTime()` or `jest.advanceTimersToNextTimer()`. See the
[API documentation](/docs/jest-object#mock-timers) for more information.
You can also have more fine-grained control with for example `jest.advanceTimersByTime()` or `jest.advanceTimersToNextTimer()`. See the [API documentation](/docs/jest-object#mock-timers) for more information.

## Faking timers is a global operation

Calling `jest.useRealTimers()` will turn on fake timers for all tests within
the same file, until normal timers are restored with `jest.useRealTimers()`.
Calling `jest.useRealTimers()` will turn on fake timers for all tests within the same file, until normal timers are restored with `jest.useRealTimers()`.

The fake timers also have a global state that only resets each time you call
`jest.useFakeTimers()`.
The fake timers also have a global state that only resets each time you call `jest.useFakeTimers()`.

So while `jest.useFakeTimers()` and `jest.useRealTimers()` can be called from
anywhere (top-level, inside a `test` block, etc.), you need to be careful
in order to avoid unexpected behavior.
So while `jest.useFakeTimers()` and `jest.useRealTimers()` can be called from anywhere (top-level, inside a `test` block, etc.), you need to be careful in order to avoid unexpected behavior.

```js
test('do something with fake timers', () => {
Expand All @@ -97,10 +80,7 @@ test('do something with real timers (?)', () => {
});
```

In this example, since we never call `jest.useRealTimers()`, both tests
would end up using fake timers when run synchronously. And the state
(time/internal counters) of the fake timers would leak across since we did not
reset it.
In this example, since we never call `jest.useRealTimers()`, both tests would end up using fake timers when run synchronously. And the state (time/internal counters) of the fake timers would leak across since we did not reset it.

A better approach could be:

Expand All @@ -125,30 +105,20 @@ test('do something with real timers', () => {
});
```

Here we use an `afterEach` to call `jest.useRealTimers()` after every single
test.
Here we use an `afterEach` to call `jest.useRealTimers()` after every single test.

This pattern can help keep behavior predictable as your test file
grows.
This pattern can help keep behavior predictable as your test file grows.

- It will establish a clear baseline ->
"timers are real unless otherwise is set".
- The state of the fake timers is always reset between tests, since all tests
wanting to use them have to call `jest.useFakeTimers()` first.
- It will establish a clear baseline -> "timers are real unless otherwise is set".
- The state of the fake timers is always reset between tests, since all tests wanting to use them have to call `jest.useFakeTimers()` first.

## Handling recursive timers

Scenarios exist where you might have a recursive timer -- for example a function
that sets a timer to call the same function again.
Scenarios exist where you might have a recursive timer -- for example a function that sets a timer to call the same function again.

If you use `jest.runAllTimers()` to advance time, it will execute all pending
tasks. If those tasks themselves schedule new tasks, those will be continually
exhausted until there are no more tasks remaining in the queue. So with a
recursive timer, you could end up with an infinite loop.
If you use `jest.runAllTimers()` to advance time, it will execute all pending tasks. If those tasks themselves schedule new tasks, those will be continually exhausted until there are no more tasks remaining in the queue. So with a recursive timer, you could end up with an infinite loop.

To solve this, Jest ships with an alternative called
`jest.runOnlyPendingTimers()`. This will run only the tasks queued by
`setTimeout()` or `setInterval()` up until that point.
To solve this, Jest ships with an alternative called `jest.runOnlyPendingTimers()`. This will run only the tasks queued by `setTimeout()` or `setInterval()` up until that point.

```js title="/examples/timer/modern/infiniteTimerTest.js"
function infiniteTimerTest(callback) {
Expand Down Expand Up @@ -177,8 +147,8 @@ test('should not start recursive timer loop', () => {

infiniteTimerTest(callback);

// At this point only the timer in infiniteTimerTest() is scheduled,
// and time is frozen. So infiniteTimer() or the callback passed to it
// At this point only the timer in infiniteTimerTest() is scheduled,
// and time is frozen. So infiniteTimer() or the callback passed to it
// should not have been invoked.
expect(callback).not.toHaveBeenCalled();

Expand Down Expand Up @@ -211,21 +181,10 @@ TODO: Info about / link to more code examples here.

### Timer functions are no longer a mock or spy function. How can I assert against them?

In the legacy fake timers, prior to Jest 27, native timer functions were
replaced by Jest mock functions. After moving to a new implementation based on
an [external library][fake-timers-github], this is no longer a feature. So the
short answer is that you can't... at least not directly.
In the legacy fake timers, prior to Jest 27, native timer functions were replaced by Jest mock functions. After moving to a new implementation based on an [external library][fake-timers-github], this is no longer a feature. So the short answer is that you can't... at least not directly.

The specific timer function used should be considered an implementation detail,
and it would generally be more robust to test against what you expect to happen
when a task runs.
The specific timer function used should be considered an implementation detail, and it would generally be more robust to test against what you expect to happen when a task runs.

Looking back at the first code example on this page, you will see that we
assert if the mocked callback function is called with the parameter we expect.
This decouples us from the details of what happens inside `timerTest()`. At
some point in the future, we can safely replace `setTimeout()` with another
timer without invalidating the test.
Looking back at the first code example on this page, you will see that we assert if the mocked callback function is called with the parameter we expect. This decouples us from the details of what happens inside `timerTest()`. At some point in the future, we can safely replace `setTimeout()` with another timer without invalidating the test.

If you need to verify that callbacks are scheduled with a particular time or
interval, consider using `jest.advanceTimersByTime()` and make assertions based
on what you expect at different points in time.
If you need to verify that callbacks are scheduled with a particular time or interval, consider using `jest.advanceTimersByTime()` and make assertions based on what you expect at different points in time.

0 comments on commit aebeabf

Please sign in to comment.