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!