How setTimeout can help with testing asynchronous stuff in React components
tldr;
Learning how good old setTimeout()
can be utilized to resolve promises before making any assertions while testing
asynchronous code in React components.
[tested on react 16.8.6 with enzyme 3.7.0]
What we are aiming for
In today's web apps making all kinds of HTTP requests is an integral part of preserving activity between client and server. When the feature is so widely used across an app we want to be sure it's working as desired. That's what writing tests often helps us with. In general React component re-renders when state (not only state but let's keep it very simple here) has changed.
In the case we discuss, we want to check whether results render correctly
based on how (and if) the request was resolved. We've got the plan, but there's
one thing that's stopping us. Enzyme test runner doesn't wait for promises to
resolve thus execution of a test case finishes before we get a chance to see
any response! That's a time for setTimeout
to give us helping hand. We'll be
able to resolve any running promises inside a test case now!
How to achieve it
Before we start with the example let's do a quick recap on how javascript async
flow works. In javascript promises are used to represent completion/failure of
asynchronous operations. Promise can either be resolved (request succeeded,
response handled inside .then()
method) or rejected (request failed, errors
handled inside .catch()
method).
Let's quickly build very simple react functional component which contains:
-
button which fires
async getUsers
function on click -
div
which reflects one of the three states:-
renders a paragraph reading 'No users yet.' when there is no data and API call has not been fired yet
-
renders a paragraph with 'loading...' text when API call has been fired and promise is not resolved yet
-
renders a paragraph for each user when the promise was resolved successfully (we don't care about the potential error handling in this example)
-
-
async getUsers
function which (a)waits with the state change execution until the promise is resolved (or rejected and then executes code inside the catch block)
import React, { useState } from 'react'
export const Example = () => {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(false)
const getUsers = async () => {
try {
const users = await window
.fetch('https://api.example.com/users/')
.then((data) => data.json())
setUsers(users)
setLoading(false)
}
catch (error) {
setLoading(false)
}
}
const onClick = () => {
setLoading(true)
getUsers()
}
return (
<div>
<button className="get-users-btn" onClick={onClick}>
Show users
</button>
<div className="content">
{loading ? (
<p className="loading">loading...</p>
) : users?.length ? (
users.map(user => (<p className="user-item" key={user.id}>{user.name}</p>))
) : (
<p className="no-users">No users yet.</p>
)}
</div>
</div>
)
}
In the example above we're using asynchronous function, created by adding
async
keyword before the function name. Additionally, we'll combine it with
await
keyword. When await
is put in front of promise-based function (and
function declared with async
keyword always returns a promise!), the function
behaves as synchronous one and waits until the returned promise is
resolved or rejected. When we look at the code below we can see that
component's state changes depending on promise result.
Now let's write an unit test for that component.
import React from 'react'
import { shallow } from 'enzyme'
import { Example } from '_r/components/Example'
// we'll discuss this line later on
const flushPromises = () => new Promise(resolve => {
setTimeout(resolve, 0)
})
it('should render a paragraph for each user', async () => {
const mockData = [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' },
{ id: 3, name: 'User 3' },
]
jest.spyOn(window, 'fetch').mockReturnValue(
Promise.resolve({ json: () => Promise.resolve(mockData) })
)
const component = shallow(<Example />)
expect(component.find('button').length).toBe(1)
expect(component.find('.no-users').length).toBe(1)
component.find('button').simulate('click')
expect(component.find('.user-item').length).toBe(3)
})
Initially, we want to mock window.fetch
method to be resolved successfully
with given mockData
array as a returned value, so we set up both promises to
be resolved successfully. First one fetches the data and the second one (inside
.then()
block) converts the data to JSON format. The next step is to render our
Example component and make some assertions. Firstly, we check if the button and
'no-users' text were rendered. Secondly, we simulate the button click so the
request inside onClick
function can be called. Finally, we check if every user
has its own paragraph rendered successfully.
The test has failed. Why? Because jest
runs code synchronously so it doesn't
wait for promises to be resolved/rejected. To confirm that getUsers
method has
been called (but not resolved) we can add following assertion
component.find('button').simulate('click')
expect(component.find('.loading').length).toBe(1)
// expect(component.find('.user-item').length).toBe(3)
and see the test passing.
Now the goal is to resolve promises before running assertions somehow. That's
the time for setTimeout
to shine. We can use it to flush all queued promises.
In the example above you could've seen the function already:
const flushPromises = () => new Promise(resolve => {
setTimeout(resolve, 0)
})
Let's analyze step by step what's going on here. setTimeout
will resolve
a promise as soon as possible ((delay=0)
), that means: "as soon as the stack
is empty, not immediately" according to docs. When we call await
on top of
it, we ensure the rest of the test code (assertions) is put at the end of the
promise jobs queue. That allows rest of the promises, in this case ones inside
our async getUsers
function, to be resolved first. This way we got exactly
what we needed! Now let's update our test case accordingly.
component.find('button').simulate('click')
await flushPromises()
expect(component.find('.user-item').length).toBe(3)
and run assertions once again:
And the test is green again, this time with the request's response data rendered on the UI. We made it!
Conclusion
Remember that setTimeout
is your testing buddy! setTimeout
makes the test
runner wait until you're finished with your promises business. That's what
friends do, they give you a helping hand when you need it!