Jest timers and reactjs - A use case for using jest time related functions
The content here is under the Attribution 4.0 International (CC BY 4.0) license
This post explores the possibilities to use fake timers with jest. Such feature is needed for async testing, whereas is needed to await for some computation to finish. Such computation can vary on time, for example, 1 second, or 10 minutes. In a test case scenario, the waiting for this computation is not needed [1].
Jest offers fake times, to advance in time or run pending computations without the need to wait for them to complete. Jest is not used in reactjs projects only, therefore, the examples used here use reactjs as a library to build web interfaces. For pure nodejs examples with times it is recommended to go to the official documentation [1].
Dave Farley in his webinar about acceptance testing also recommends this approach for controlling time variables [2], Dave shows code examples in java, which spot this idea of controlling time across programming languages, the concept is the important bit.
NOTE: this post assumes testing knowledge as well as jest features, such as: describe and test.
Context
The react component used in this post is a component that display different content based on the props given, and also, it uses animation to introduce them. as such, the animation takes time to finish and to complete its life cycle. Jest timers are used to avoid the needed to delay the test suite execution. For further details on the test file and implementation refer to:
Fake times
The first step to use the fake timers is to have them set. There are two options
to achieve that. The first one is to call useFakeTimers
:
jest.useFakeTimers();
describe('my test suite', () => {
// test cases
})
The second options is to set the function inside the beforeEach
:
describe('my test suite', () => {
beforeEach(() => {
jest.useFakeTimers();
})
// test cases
})
The second option is the preferred one if the test suite has multiple tests that depends on the timer, as the official jest documentation says:
If running multiple tests inside of one file or describe block, jest.useFakeTimers() can be called before each test manually or with a setup function such as beforeEach. Not doing so will result in the internal usage counter not being reset.
On the other hand, the first approach has no way to reset the mocks set, for achieve that we would have to make use of the function afterAll from jest. All in all, the approach used for the post is the following:
describe('my test suite', () => {
beforeEach(() => {
jest.useFakeTimers()
})
afterEach(() => {
jest.useRealTimers()
})
// test cases
})
Run all timers
import { mount } from 'enzyme'
import { act } from 'react-dom/test-utils'
import Scene from './Scene'
describe('Scene component', () => {
beforeEach(() => {
jest.useFakeTimers();
})
afterEach(() => {
jest.useRealTimers();
})
test('should handle next scene', () => {
const handleLastScene = jest.fn()
const handleNextScene = jest.fn()
const wrapper = mount(
<Scene
lastScene={false}
handleLastScene={handleLastScene}
next={handleNextScene}
text={fakeText}
showNextButton={1} // Time in milliseconds to display the button
releaseButton={1} // TIme in milliseconds to enable the interaction with the button
/>
)
act(() => {
jest.runAllTimers(); // Run all peding timers (setTimeout, setInterval, clearTimeout, clearInterval)
})
wrapper.update()
wrapper.find(Button).simulate('click')
expect(handleLastScene).toHaveBeenCalledTimes(0)
expect(handleNextScene).toHaveBeenCalled()
})
})
In the test case, the button depends on two specific timers, one for displaying the button and the other one to enable the interaction with the button again. The latter is to prevent to call the button events twice or even more times.
runAllTimers
as specified in the jest documentation, is desired to use
when there is no recursive timers, otherwise runAllTimers
will end up
in a endless loop [1].
Advance timers by time
Another approach to use in the test case provided in the previous section
would be to use advanceTimersByTime
. As long as the time provided by the
function is greater than the time passed in showNextButton
, it should
work. For example:
// imports and describe have been removed, this snippet
// focus on jest.advanceTimersByTime only
test('should handle next scene', () => {
const handleLastScene = jest.fn()
const handleNextScene = jest.fn()
const wrapper = mount(
<Scene
lastScene={false}
handleLastScene={handleLastScene}
next={handleNextScene}
text={fakeText}
showNextButton={1}
releaseButton={1}
/>
)
act(() => {
jest.advanceTimersByTime(2000); // advance time by 2 seconds
})
wrapper.update()
wrapper.find(Button).simulate('click')
expect(handleLastScene).toHaveBeenCalledTimes(0)
expect(handleNextScene).toHaveBeenCalled()
})
Using advanceTimersByTime
advance all timers as well as recursive timers. Both
of the approaches depicted work without having to wait for the actual time
to pass, thus, the test suite runs as fast as it can and there is no
“real” delay.
References
- [1]Jest, “Timer Mocks,” 2021 [Online]. Available at: https://jestjs.io/docs/timer-mocks. [Accessed: 04-Apr-2021]
- [2]D. Farley, “Acceptance Testing | Webinar,” 2021 [Online]. Available at: https://youtu.be/SuDIYk9GBpE?t=1828. [Accessed: 17-Feb-2021]
Table of contents
Got a question?
If you have question or feedback, don't think twice and click here to leave a comment. Just want to support me? Buy me a coffee!