From 9e5c1b3f9552ecbb362409d96b17457ee53794b5 Mon Sep 17 00:00:00 2001 From: Sigve Hoel <6737410+sigveio@users.noreply.github.com> Date: Wed, 4 Aug 2021 19:00:41 +0200 Subject: [PATCH 1/5] fix(website): fix bg of Note admonitions in dark mode Use a darker background color for Note style admonitions in dark mode to reduce eye strain. --- CHANGELOG.md | 2 ++ website/src/css/docusaurusTheme.css | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0af283259c24..7ef94e333677 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ### Chore & Maintenance +- `[website]` Use a darker background color for Note style admonitions in dark mode + ### Performance ## 27.1.0 diff --git a/website/src/css/docusaurusTheme.css b/website/src/css/docusaurusTheme.css index 0d7b8843d878..6e619db5d912 100644 --- a/website/src/css/docusaurusTheme.css +++ b/website/src/css/docusaurusTheme.css @@ -70,3 +70,10 @@ html[data-theme='dark'] .footer--dark { .footer--dark .text--center { color: var(--grey); } + +/** less bright background for Note style admonitions in dark theme **/ +html[data-theme='dark'] .alert--secondary { + --ifm-alert-background-color: var(--ifm-color-emphasis-300); + --ifm-alert-border-color: var(--ifm-color-emphasis-300); + --ifm-alert-color: var(--ifm-color-black); +} From 8b8874b174a4e75f2da6ef6a1f8ed9fa22a249df Mon Sep 17 00:00:00 2001 From: Sigve Hoel <6737410+sigveio@users.noreply.github.com> Date: Wed, 4 Aug 2021 19:04:56 +0200 Subject: [PATCH 2/5] feat(website): add CSS for highlighting in code blocks --- CHANGELOG.md | 1 + website/src/css/docusaurusTheme.css | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ef94e333677..972aebe58d43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Chore & Maintenance - `[website]` Use a darker background color for Note style admonitions in dark mode +- `[website]` Add CSS for line highlighting in docusaurus code blocks ### Performance diff --git a/website/src/css/docusaurusTheme.css b/website/src/css/docusaurusTheme.css index 6e619db5d912..990780f71afb 100644 --- a/website/src/css/docusaurusTheme.css +++ b/website/src/css/docusaurusTheme.css @@ -77,3 +77,14 @@ html[data-theme='dark'] .alert--secondary { --ifm-alert-border-color: var(--ifm-color-emphasis-300); --ifm-alert-color: var(--ifm-color-black); } + +.docusaurus-highlight-code-line { + background-color: var(--ifm-color-emphasis-300); + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 var(--ifm-pre-padding); +} + +html[data-theme='dark'] .docusaurus-highlight-code-line { + background-color: var(--ifm-color-emphasis-100); +} From 282e14d23ade927cd8e3e5b6f0e9a4f83345fcfe Mon Sep 17 00:00:00 2001 From: Sigve Hoel <6737410+sigveio@users.noreply.github.com> Date: Wed, 4 Aug 2021 19:07:30 +0200 Subject: [PATCH 3/5] docs: update Timer Mocks guide for modern timers --- CHANGELOG.md | 1 + docs/TimerMocks.md | 279 +++++++++++------- .../versioned_docs/version-27.0/TimerMocks.md | 279 +++++++++++------- 3 files changed, 333 insertions(+), 226 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 972aebe58d43..4c2e86f736b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - `[website]` Use a darker background color for Note style admonitions in dark mode - `[website]` Add CSS for line highlighting in docusaurus code blocks +- `[docs]` Update Timer Mocks guide for Jest 27 to be in line with the new default `modern` fake timers. ### Performance diff --git a/docs/TimerMocks.md b/docs/TimerMocks.md index 5170e399a977..95beadb70914 100644 --- a/docs/TimerMocks.md +++ b/docs/TimerMocks.md @@ -3,44 +3,110 @@ 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!](https://www.youtube.com/watch?v=QZoJ2Pt27BY) +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] -```javascript -// timerGame.js -'use strict'; +[back-to-the-future-reference]: https://www.youtube.com/watch?v=QZoJ2Pt27BY -function timerGame(callback) { - console.log('Ready....go!'); - setTimeout(() => { - console.log("Time's up -- stop!"); - callback && callback(); - }, 1000); +:::note + +The default timer implementation changed in Jest 27 and is now based on +[@sinonjs/fake-timers][fake-timers-github]. + +::: + +[fake-timers-github]: https://github.com/sinonjs/fake-timers + +## Enabling fake timers + +```js title="/examples/timer/modern/timerTest.js" +function timerTest(callback) { + setTimeout(() => callback('Timer finished!'), 10000); } +module.exports = timerTest; +``` + +```js title="/examples/timer/modern/__tests__/timerTest.spec.js" +test('should invoke callback after timer ends', () => { + const timerTest = require('../timerTest'); + const callback = jest.fn(); + + // highlight-start + // Enable mocking of native timer functions + jest.useFakeTimers(); + // highlight-end + + // Add a 10 second timer to invoke a mocked callback function + timerTest(callback); + + // The callback should not have been called yet + expect(callback).not.toHaveBeenCalled(); + + // highlight-start + // Fast-forward until all timers have been executed + jest.runAllTimers(); + // highlight-end -module.exports = timerGame; + // Assert successfully without having to wait for the 10 second delay + expect(callback).toBeCalledWith('Timer finished!'); +}); ``` -```javascript -// __tests__/timerGame-test.js -'use strict'; +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). + +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. -jest.useFakeTimers(); +## Faking timers is a global operation -test('waits 1 second before ending the game', () => { - const timerGame = require('../timerGame'); - timerGame(); +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()`. + +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', () => { + jest.useFakeTimers(); + // ... +}); - expect(setTimeout).toHaveBeenCalledTimes(1); - expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); +test('do something with real timers (?)', () => { + // This would still use fake timers }); ``` -Here we enable fake timers by calling `jest.useFakeTimers()`. This mocks out `setTimeout` and other timer functions with mock functions. Timers can be restored to their normal behavior with `jest.useRealTimers()`. +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. -While you can call `jest.useFakeTimers()` or `jest.useRealTimers()` from anywhere (top level, inside an `it` block, etc.), it is a **global operation** and will affect other tests within the same file. Additionally, you need to call `jest.useFakeTimers()` to reset internal counters before each test. If you plan to not use fake timers in all your tests, you will want to clean up manually, as otherwise the faked timers will leak across tests: +A better approach could be: -```javascript +```js afterEach(() => { + // Reset to real timers after each test jest.useRealTimers(); }); @@ -49,130 +115,117 @@ test('do something with fake timers', () => { // ... }); +test('do something else with fake timers', () => { + jest.useFakeTimers(); + // ... +}); + test('do something with real timers', () => { // ... }); ``` -## Run All Timers - -Another test we might want to write for this module is one that asserts that the callback is called after 1 second. To do this, we're going to use Jest's timer control APIs to fast-forward time right in the middle of the test: - -```javascript -test('calls the callback after 1 second', () => { - const timerGame = require('../timerGame'); - const callback = jest.fn(); - - timerGame(callback); +Here we use an `afterEach` to call `jest.useRealTimers()` after every single +test. - // At this point in time, the callback should not have been called yet - expect(callback).not.toBeCalled(); +This pattern can help keep behavior predictable as your test file +grows. - // Fast-forward until all timers have been executed - jest.runAllTimers(); +- 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. - // Now our callback should have been called! - expect(callback).toBeCalled(); - expect(callback).toHaveBeenCalledTimes(1); -}); -``` +## Handling recursive timers -## Run Pending Timers +Scenarios exist where you might have a recursive timer -- for example a function +that sets a timer to call the same function again. -There are also scenarios where you might have a recursive timer -- that is a timer that sets a new timer in its own callback. For these, running all the timers would be an endless loop… so something like `jest.runAllTimers()` is not desirable. For these cases you might use `jest.runOnlyPendingTimers()`: +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. -```javascript -// infiniteTimerGame.js -'use strict'; +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. -function infiniteTimerGame(callback) { - console.log('Ready....go!'); +```js title="/examples/timer/modern/infiniteTimerTest.js" +function infiniteTimerTest(callback) { + // Schedule the infiniteTimer() to start in 10 seconds + setTimeout(() => infiniteTimer(callback), 10000); +} +function infiniteTimer(callback) { + callback('infiniteTimer: start'); setTimeout(() => { - console.log("Time's up! 10 seconds before the next game starts..."); - callback && callback(); - - // Schedule the next game in 10 seconds - setTimeout(() => { - infiniteTimerGame(callback); - }, 10000); - }, 1000); + callback('infiniteTimer: setTimeout'); + // Invoke itself to immediately schedule another timer in a recursive loop + infiniteTimer(callback); + }, 10000); } -module.exports = infiniteTimerGame; +module.exports = infiniteTimerTest; ``` -```javascript -// __tests__/infiniteTimerGame-test.js -'use strict'; - -jest.useFakeTimers(); - -describe('infiniteTimerGame', () => { - test('schedules a 10-second timer after 1 second', () => { - const infiniteTimerGame = require('../infiniteTimerGame'); - const callback = jest.fn(); +```js title="/examples/timer/modern/__tests__/infiniteTimerTest.spec.js" +test('should not start recursive timer loop', () => { + const infiniteTimerTest = require('../infiniteTimerTest'); + const callback = jest.fn(); - infiniteTimerGame(callback); + jest.useFakeTimers(); - // At this point in time, there should have been a single call to - // setTimeout to schedule the end of the game in 1 second. - expect(setTimeout).toHaveBeenCalledTimes(1); - expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); + infiniteTimerTest(callback); - // Fast forward and exhaust only currently pending timers - // (but not any new timers that get created during that process) - jest.runOnlyPendingTimers(); + // 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(); - // At this point, our 1-second timer should have fired it's callback - expect(callback).toBeCalled(); + // highlight-start + jest.runOnlyPendingTimers(); + // highlight-end - // And it should have created a new timer to start the game over in - // 10 seconds - expect(setTimeout).toHaveBeenCalledTimes(2); - expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 10000); - }); + // Now the callback should have been invoked only once, at the beginning of + // infiniteTimer(). The setTimeout() within should have been ignored. + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith('infiniteTimer: start'); }); ``` -## Advance Timers by Time +Thanks to using `jest.runOnlyPendingTimers()`, the test finishes successfully. -Another possibility is use `jest.advanceTimersByTime(msToRun)`. When this API is called, all timers are advanced by `msToRun` milliseconds. All pending "macro-tasks" that have been queued via setTimeout() or setInterval(), and would be executed during this time frame, will be executed. Additionally, if those macro-tasks schedule new macro-tasks that would be executed within the same time frame, those will be executed until there are no more macro-tasks remaining in the queue that should be run within msToRun milliseconds. +If you replace it with `jest.runAllTimers()`, you will see that Jest throws an error: -```javascript -// timerGame.js -'use strict'; +```error +Aborting after running 100000 timers, assuming an infinite loop! +``` -function timerGame(callback) { - console.log('Ready....go!'); - setTimeout(() => { - console.log("Time's up -- stop!"); - callback && callback(); - }, 1000); -} +Yikes... that's a lot of timers! -module.exports = timerGame; -``` +## More code examples -```javascript -it('calls the callback after 1 second via advanceTimersByTime', () => { - const timerGame = require('../timerGame'); - const callback = jest.fn(); +TODO: Info about / link to more code examples here. - timerGame(callback); +## Frequently Asked Questions - // At this point in time, the callback should not have been called yet - expect(callback).not.toBeCalled(); +### Timer functions are no longer a mock or spy function. How can I assert against them? - // Fast-forward until all timers have been executed - jest.advanceTimersByTime(1000); +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. - // Now our callback should have been called! - expect(callback).toBeCalled(); - expect(callback).toHaveBeenCalledTimes(1); -}); -``` +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. -Lastly, it may occasionally be useful in some tests to be able to clear all of the pending timers. For this, we have `jest.clearAllTimers()`. +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. -The code for this example is available at [examples/timer](https://github.com/facebook/jest/tree/master/examples/timer). +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. diff --git a/website/versioned_docs/version-27.0/TimerMocks.md b/website/versioned_docs/version-27.0/TimerMocks.md index 5170e399a977..95beadb70914 100644 --- a/website/versioned_docs/version-27.0/TimerMocks.md +++ b/website/versioned_docs/version-27.0/TimerMocks.md @@ -3,44 +3,110 @@ 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!](https://www.youtube.com/watch?v=QZoJ2Pt27BY) +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] -```javascript -// timerGame.js -'use strict'; +[back-to-the-future-reference]: https://www.youtube.com/watch?v=QZoJ2Pt27BY -function timerGame(callback) { - console.log('Ready....go!'); - setTimeout(() => { - console.log("Time's up -- stop!"); - callback && callback(); - }, 1000); +:::note + +The default timer implementation changed in Jest 27 and is now based on +[@sinonjs/fake-timers][fake-timers-github]. + +::: + +[fake-timers-github]: https://github.com/sinonjs/fake-timers + +## Enabling fake timers + +```js title="/examples/timer/modern/timerTest.js" +function timerTest(callback) { + setTimeout(() => callback('Timer finished!'), 10000); } +module.exports = timerTest; +``` + +```js title="/examples/timer/modern/__tests__/timerTest.spec.js" +test('should invoke callback after timer ends', () => { + const timerTest = require('../timerTest'); + const callback = jest.fn(); + + // highlight-start + // Enable mocking of native timer functions + jest.useFakeTimers(); + // highlight-end + + // Add a 10 second timer to invoke a mocked callback function + timerTest(callback); + + // The callback should not have been called yet + expect(callback).not.toHaveBeenCalled(); + + // highlight-start + // Fast-forward until all timers have been executed + jest.runAllTimers(); + // highlight-end -module.exports = timerGame; + // Assert successfully without having to wait for the 10 second delay + expect(callback).toBeCalledWith('Timer finished!'); +}); ``` -```javascript -// __tests__/timerGame-test.js -'use strict'; +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). + +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. -jest.useFakeTimers(); +## Faking timers is a global operation -test('waits 1 second before ending the game', () => { - const timerGame = require('../timerGame'); - timerGame(); +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()`. + +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', () => { + jest.useFakeTimers(); + // ... +}); - expect(setTimeout).toHaveBeenCalledTimes(1); - expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); +test('do something with real timers (?)', () => { + // This would still use fake timers }); ``` -Here we enable fake timers by calling `jest.useFakeTimers()`. This mocks out `setTimeout` and other timer functions with mock functions. Timers can be restored to their normal behavior with `jest.useRealTimers()`. +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. -While you can call `jest.useFakeTimers()` or `jest.useRealTimers()` from anywhere (top level, inside an `it` block, etc.), it is a **global operation** and will affect other tests within the same file. Additionally, you need to call `jest.useFakeTimers()` to reset internal counters before each test. If you plan to not use fake timers in all your tests, you will want to clean up manually, as otherwise the faked timers will leak across tests: +A better approach could be: -```javascript +```js afterEach(() => { + // Reset to real timers after each test jest.useRealTimers(); }); @@ -49,130 +115,117 @@ test('do something with fake timers', () => { // ... }); +test('do something else with fake timers', () => { + jest.useFakeTimers(); + // ... +}); + test('do something with real timers', () => { // ... }); ``` -## Run All Timers - -Another test we might want to write for this module is one that asserts that the callback is called after 1 second. To do this, we're going to use Jest's timer control APIs to fast-forward time right in the middle of the test: - -```javascript -test('calls the callback after 1 second', () => { - const timerGame = require('../timerGame'); - const callback = jest.fn(); - - timerGame(callback); +Here we use an `afterEach` to call `jest.useRealTimers()` after every single +test. - // At this point in time, the callback should not have been called yet - expect(callback).not.toBeCalled(); +This pattern can help keep behavior predictable as your test file +grows. - // Fast-forward until all timers have been executed - jest.runAllTimers(); +- 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. - // Now our callback should have been called! - expect(callback).toBeCalled(); - expect(callback).toHaveBeenCalledTimes(1); -}); -``` +## Handling recursive timers -## Run Pending Timers +Scenarios exist where you might have a recursive timer -- for example a function +that sets a timer to call the same function again. -There are also scenarios where you might have a recursive timer -- that is a timer that sets a new timer in its own callback. For these, running all the timers would be an endless loop… so something like `jest.runAllTimers()` is not desirable. For these cases you might use `jest.runOnlyPendingTimers()`: +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. -```javascript -// infiniteTimerGame.js -'use strict'; +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. -function infiniteTimerGame(callback) { - console.log('Ready....go!'); +```js title="/examples/timer/modern/infiniteTimerTest.js" +function infiniteTimerTest(callback) { + // Schedule the infiniteTimer() to start in 10 seconds + setTimeout(() => infiniteTimer(callback), 10000); +} +function infiniteTimer(callback) { + callback('infiniteTimer: start'); setTimeout(() => { - console.log("Time's up! 10 seconds before the next game starts..."); - callback && callback(); - - // Schedule the next game in 10 seconds - setTimeout(() => { - infiniteTimerGame(callback); - }, 10000); - }, 1000); + callback('infiniteTimer: setTimeout'); + // Invoke itself to immediately schedule another timer in a recursive loop + infiniteTimer(callback); + }, 10000); } -module.exports = infiniteTimerGame; +module.exports = infiniteTimerTest; ``` -```javascript -// __tests__/infiniteTimerGame-test.js -'use strict'; - -jest.useFakeTimers(); - -describe('infiniteTimerGame', () => { - test('schedules a 10-second timer after 1 second', () => { - const infiniteTimerGame = require('../infiniteTimerGame'); - const callback = jest.fn(); +```js title="/examples/timer/modern/__tests__/infiniteTimerTest.spec.js" +test('should not start recursive timer loop', () => { + const infiniteTimerTest = require('../infiniteTimerTest'); + const callback = jest.fn(); - infiniteTimerGame(callback); + jest.useFakeTimers(); - // At this point in time, there should have been a single call to - // setTimeout to schedule the end of the game in 1 second. - expect(setTimeout).toHaveBeenCalledTimes(1); - expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); + infiniteTimerTest(callback); - // Fast forward and exhaust only currently pending timers - // (but not any new timers that get created during that process) - jest.runOnlyPendingTimers(); + // 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(); - // At this point, our 1-second timer should have fired it's callback - expect(callback).toBeCalled(); + // highlight-start + jest.runOnlyPendingTimers(); + // highlight-end - // And it should have created a new timer to start the game over in - // 10 seconds - expect(setTimeout).toHaveBeenCalledTimes(2); - expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 10000); - }); + // Now the callback should have been invoked only once, at the beginning of + // infiniteTimer(). The setTimeout() within should have been ignored. + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith('infiniteTimer: start'); }); ``` -## Advance Timers by Time +Thanks to using `jest.runOnlyPendingTimers()`, the test finishes successfully. -Another possibility is use `jest.advanceTimersByTime(msToRun)`. When this API is called, all timers are advanced by `msToRun` milliseconds. All pending "macro-tasks" that have been queued via setTimeout() or setInterval(), and would be executed during this time frame, will be executed. Additionally, if those macro-tasks schedule new macro-tasks that would be executed within the same time frame, those will be executed until there are no more macro-tasks remaining in the queue that should be run within msToRun milliseconds. +If you replace it with `jest.runAllTimers()`, you will see that Jest throws an error: -```javascript -// timerGame.js -'use strict'; +```error +Aborting after running 100000 timers, assuming an infinite loop! +``` -function timerGame(callback) { - console.log('Ready....go!'); - setTimeout(() => { - console.log("Time's up -- stop!"); - callback && callback(); - }, 1000); -} +Yikes... that's a lot of timers! -module.exports = timerGame; -``` +## More code examples -```javascript -it('calls the callback after 1 second via advanceTimersByTime', () => { - const timerGame = require('../timerGame'); - const callback = jest.fn(); +TODO: Info about / link to more code examples here. - timerGame(callback); +## Frequently Asked Questions - // At this point in time, the callback should not have been called yet - expect(callback).not.toBeCalled(); +### Timer functions are no longer a mock or spy function. How can I assert against them? - // Fast-forward until all timers have been executed - jest.advanceTimersByTime(1000); +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. - // Now our callback should have been called! - expect(callback).toBeCalled(); - expect(callback).toHaveBeenCalledTimes(1); -}); -``` +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. -Lastly, it may occasionally be useful in some tests to be able to clear all of the pending timers. For this, we have `jest.clearAllTimers()`. +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. -The code for this example is available at [examples/timer](https://github.com/facebook/jest/tree/master/examples/timer). +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. From 3a1c2309baddc35c50dcb96e354eb24cde2b53a6 Mon Sep 17 00:00:00 2001 From: Sigve Hoel <6737410+sigveio@users.noreply.github.com> Date: Mon, 16 Aug 2021 15:45:07 +0200 Subject: [PATCH 4/5] fixup! docs: update Timer Mocks guide for modern timers --- docs/TimerMocks.md | 89 +++++-------------- .../versioned_docs/version-27.0/TimerMocks.md | 89 +++++-------------- 2 files changed, 48 insertions(+), 130 deletions(-) diff --git a/docs/TimerMocks.md b/docs/TimerMocks.md index 95beadb70914..1a8a2e696092 100644 --- a/docs/TimerMocks.md +++ b/docs/TimerMocks.md @@ -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]. ::: @@ -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; ``` @@ -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', () => { @@ -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: @@ -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) { @@ -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(); @@ -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. diff --git a/website/versioned_docs/version-27.0/TimerMocks.md b/website/versioned_docs/version-27.0/TimerMocks.md index 95beadb70914..1a8a2e696092 100644 --- a/website/versioned_docs/version-27.0/TimerMocks.md +++ b/website/versioned_docs/version-27.0/TimerMocks.md @@ -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]. ::: @@ -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; ``` @@ -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', () => { @@ -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: @@ -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) { @@ -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(); @@ -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. From cbe8143b495c69be88c6d85b9ab7ee5dc711d32f Mon Sep 17 00:00:00 2001 From: Sigve Hoel <6737410+sigveio@users.noreply.github.com> Date: Tue, 31 Aug 2021 15:21:30 +0200 Subject: [PATCH 5/5] fixup! docs: update Timer Mocks guide for modern timers --- docs/TimerMocks.md | 6 +- .../versioned_docs/version-27.0/TimerMocks.md | 6 +- .../versioned_docs/version-27.1/TimerMocks.md | 240 +++++++++--------- 3 files changed, 135 insertions(+), 117 deletions(-) diff --git a/docs/TimerMocks.md b/docs/TimerMocks.md index 1a8a2e696092..19e6688e851e 100644 --- a/docs/TimerMocks.md +++ b/docs/TimerMocks.md @@ -181,10 +181,12 @@ 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. -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. +You can still spy on the global yourself after enabling the fake timers, but 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. 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. + +TODO: Polish this... diff --git a/website/versioned_docs/version-27.0/TimerMocks.md b/website/versioned_docs/version-27.0/TimerMocks.md index 1a8a2e696092..19e6688e851e 100644 --- a/website/versioned_docs/version-27.0/TimerMocks.md +++ b/website/versioned_docs/version-27.0/TimerMocks.md @@ -181,10 +181,12 @@ 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. -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. +You can still spy on the global yourself after enabling the fake timers, but 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. 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. + +TODO: Polish this... diff --git a/website/versioned_docs/version-27.1/TimerMocks.md b/website/versioned_docs/version-27.1/TimerMocks.md index 5170e399a977..19e6688e851e 100644 --- a/website/versioned_docs/version-27.1/TimerMocks.md +++ b/website/versioned_docs/version-27.1/TimerMocks.md @@ -3,44 +3,90 @@ 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!](https://www.youtube.com/watch?v=QZoJ2Pt27BY) +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] -```javascript -// timerGame.js -'use strict'; +[back-to-the-future-reference]: https://www.youtube.com/watch?v=QZoJ2Pt27BY -function timerGame(callback) { - console.log('Ready....go!'); - setTimeout(() => { - console.log("Time's up -- stop!"); - callback && callback(); - }, 1000); +:::note + +The default timer implementation changed in Jest 27 and is now based on [@sinonjs/fake-timers][fake-timers-github]. + +::: + +[fake-timers-github]: https://github.com/sinonjs/fake-timers + +## Enabling fake timers + +```js title="/examples/timer/modern/timerTest.js" +function timerTest(callback) { + setTimeout(() => callback('Timer finished!'), 10000); } -module.exports = timerGame; +module.exports = timerTest; ``` -```javascript -// __tests__/timerGame-test.js -'use strict'; +```js title="/examples/timer/modern/__tests__/timerTest.spec.js" +test('should invoke callback after timer ends', () => { + const timerTest = require('../timerTest'); + const callback = jest.fn(); -jest.useFakeTimers(); + // highlight-start + // Enable mocking of native timer functions + jest.useFakeTimers(); + // highlight-end -test('waits 1 second before ending the game', () => { - const timerGame = require('../timerGame'); - timerGame(); + // Add a 10 second timer to invoke a mocked callback function + timerTest(callback); + + // The callback should not have been called yet + expect(callback).not.toHaveBeenCalled(); + + // highlight-start + // Fast-forward until all timers have been executed + jest.runAllTimers(); + // highlight-end + + // Assert successfully without having to wait for the 10 second delay + expect(callback).toBeCalledWith('Timer finished!'); +}); +``` + +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). + +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. + +## 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()`. + +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. + +```js +test('do something with fake timers', () => { + jest.useFakeTimers(); + // ... +}); - expect(setTimeout).toHaveBeenCalledTimes(1); - expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); +test('do something with real timers (?)', () => { + // This would still use fake timers }); ``` -Here we enable fake timers by calling `jest.useFakeTimers()`. This mocks out `setTimeout` and other timer functions with mock functions. Timers can be restored to their normal behavior with `jest.useRealTimers()`. +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. -While you can call `jest.useFakeTimers()` or `jest.useRealTimers()` from anywhere (top level, inside an `it` block, etc.), it is a **global operation** and will affect other tests within the same file. Additionally, you need to call `jest.useFakeTimers()` to reset internal counters before each test. If you plan to not use fake timers in all your tests, you will want to clean up manually, as otherwise the faked timers will leak across tests: +A better approach could be: -```javascript +```js afterEach(() => { + // Reset to real timers after each test jest.useRealTimers(); }); @@ -49,130 +95,98 @@ test('do something with fake timers', () => { // ... }); +test('do something else with fake timers', () => { + jest.useFakeTimers(); + // ... +}); + test('do something with real timers', () => { // ... }); ``` -## Run All Timers +Here we use an `afterEach` to call `jest.useRealTimers()` after every single test. -Another test we might want to write for this module is one that asserts that the callback is called after 1 second. To do this, we're going to use Jest's timer control APIs to fast-forward time right in the middle of the test: +This pattern can help keep behavior predictable as your test file grows. -```javascript -test('calls the callback after 1 second', () => { - const timerGame = require('../timerGame'); - const callback = jest.fn(); +- 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. - timerGame(callback); +## Handling recursive timers - // At this point in time, the callback should not have been called yet - expect(callback).not.toBeCalled(); +Scenarios exist where you might have a recursive timer -- for example a function that sets a timer to call the same function again. - // Fast-forward until all timers have been executed - jest.runAllTimers(); +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. - // Now our callback should have been called! - expect(callback).toBeCalled(); - expect(callback).toHaveBeenCalledTimes(1); -}); -``` - -## Run Pending Timers - -There are also scenarios where you might have a recursive timer -- that is a timer that sets a new timer in its own callback. For these, running all the timers would be an endless loop… so something like `jest.runAllTimers()` is not desirable. For these cases you might use `jest.runOnlyPendingTimers()`: +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. -```javascript -// infiniteTimerGame.js -'use strict'; - -function infiniteTimerGame(callback) { - console.log('Ready....go!'); +```js title="/examples/timer/modern/infiniteTimerTest.js" +function infiniteTimerTest(callback) { + // Schedule the infiniteTimer() to start in 10 seconds + setTimeout(() => infiniteTimer(callback), 10000); +} +function infiniteTimer(callback) { + callback('infiniteTimer: start'); setTimeout(() => { - console.log("Time's up! 10 seconds before the next game starts..."); - callback && callback(); - - // Schedule the next game in 10 seconds - setTimeout(() => { - infiniteTimerGame(callback); - }, 10000); - }, 1000); + callback('infiniteTimer: setTimeout'); + // Invoke itself to immediately schedule another timer in a recursive loop + infiniteTimer(callback); + }, 10000); } -module.exports = infiniteTimerGame; +module.exports = infiniteTimerTest; ``` -```javascript -// __tests__/infiniteTimerGame-test.js -'use strict'; - -jest.useFakeTimers(); - -describe('infiniteTimerGame', () => { - test('schedules a 10-second timer after 1 second', () => { - const infiniteTimerGame = require('../infiniteTimerGame'); - const callback = jest.fn(); +```js title="/examples/timer/modern/__tests__/infiniteTimerTest.spec.js" +test('should not start recursive timer loop', () => { + const infiniteTimerTest = require('../infiniteTimerTest'); + const callback = jest.fn(); - infiniteTimerGame(callback); + jest.useFakeTimers(); - // At this point in time, there should have been a single call to - // setTimeout to schedule the end of the game in 1 second. - expect(setTimeout).toHaveBeenCalledTimes(1); - expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); + infiniteTimerTest(callback); - // Fast forward and exhaust only currently pending timers - // (but not any new timers that get created during that process) - jest.runOnlyPendingTimers(); + // 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(); - // At this point, our 1-second timer should have fired it's callback - expect(callback).toBeCalled(); + // highlight-start + jest.runOnlyPendingTimers(); + // highlight-end - // And it should have created a new timer to start the game over in - // 10 seconds - expect(setTimeout).toHaveBeenCalledTimes(2); - expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 10000); - }); + // Now the callback should have been invoked only once, at the beginning of + // infiniteTimer(). The setTimeout() within should have been ignored. + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith('infiniteTimer: start'); }); ``` -## Advance Timers by Time +Thanks to using `jest.runOnlyPendingTimers()`, the test finishes successfully. -Another possibility is use `jest.advanceTimersByTime(msToRun)`. When this API is called, all timers are advanced by `msToRun` milliseconds. All pending "macro-tasks" that have been queued via setTimeout() or setInterval(), and would be executed during this time frame, will be executed. Additionally, if those macro-tasks schedule new macro-tasks that would be executed within the same time frame, those will be executed until there are no more macro-tasks remaining in the queue that should be run within msToRun milliseconds. +If you replace it with `jest.runAllTimers()`, you will see that Jest throws an error: -```javascript -// timerGame.js -'use strict'; +```error +Aborting after running 100000 timers, assuming an infinite loop! +``` -function timerGame(callback) { - console.log('Ready....go!'); - setTimeout(() => { - console.log("Time's up -- stop!"); - callback && callback(); - }, 1000); -} +Yikes... that's a lot of timers! -module.exports = timerGame; -``` +## More code examples -```javascript -it('calls the callback after 1 second via advanceTimersByTime', () => { - const timerGame = require('../timerGame'); - const callback = jest.fn(); +TODO: Info about / link to more code examples here. - timerGame(callback); +## Frequently Asked Questions - // At this point in time, the callback should not have been called yet - expect(callback).not.toBeCalled(); +### Timer functions are no longer a mock or spy function. How can I assert against them? - // Fast-forward until all timers have been executed - jest.advanceTimersByTime(1000); +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. - // Now our callback should have been called! - expect(callback).toBeCalled(); - expect(callback).toHaveBeenCalledTimes(1); -}); -``` +You can still spy on the global yourself after enabling the fake timers, but 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. + +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. -Lastly, it may occasionally be useful in some tests to be able to clear all of the pending timers. For this, we have `jest.clearAllTimers()`. +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. -The code for this example is available at [examples/timer](https://github.com/facebook/jest/tree/master/examples/timer). +TODO: Polish this...