Why this is needed
While learning to program, most of the time is focused on getting functionality to just work. When the code is related to web development, most people prefer to just open a browser and inspect the result visually. But a company that builds software needs to be sure the code doesn’t only work but is also reliable. To this end, writing tests for your code has proved to be the best way to make sure your codebase is reliable.
In the last edition of StateOfJS, a developers survey focused on the JavaScript environment, the section for testing shows that the tools available in CRA, Testing Library and Jest, are the highest-ranked by all the community, which shows how important is to know these tools and how much a good understanding is needed of using those libraries for testing.
A recap on React Testing
Newly created Create React App has support for testing. You can jump directly to your terminal and type npm test or yarn test and you will see a basic test passing.
With the tools already installed you can do a lot of testing. For example, if we create a basic component you can test its props, presence, or even HTML element ids.
const MainPage = () => (
<div className="danger" data-testid="Div::Loading">
Loading...
</div>
);
Also, you can use snapshots, which ensure the component always renders as expected or as intended by the programmer.
But in a professional environment, we can face more difficult cases and maybe you are here to learn about one particular kind: async requests in React. Let me present three common problems while testing in CRA, and then how to solve them.
Some problems with default libraries for testing in CRA
Let’s change our last code to reflect some new functionalities. For example, let's add a state, as well as some hooks to the mix in order to show a more “real world” usage. Then let's identify some problems we can find while testing this new code.
Not detecting changes in state
Our new code will have some states that would be changed on mounting:
const MainPage = () => {
const [result, setResult] = useState('')
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
(async () => {
const result: string = await fakeCall('DONE!')
setResult(result)
setIsLoading(false)
})()
}, [])
return isLoading ? (
<div className="danger" data-testid="Div::Loading">
Loading...
</div>
) : (
<div className="success" data-testid="Div::Result">
{result}
</div>
)
}
If I try to test a change in state, for example, if onLoading changed to false, I expect the element to not load but to show the result. But if I try just to search for the result element, we would see the change is not detected by Jest:
As you can notice by running the app, the state changes, it works, but Jest is not able to “see” the change.
Not detecting changes in props from Providers
Now we will consider other common problems in testing. When you have a provider way high on the tree and it changes props, this is hard to test. For example, let’s add AuthProvider, and let’s have certain component changes, for example, a certain param.
import { BrowserRouter, Route, Switch } from 'react-router-dom'
import MainPage from './pages/MainPage'
import { AuthProvider } from './context/AuthContext'
const App = () => (
<AuthProvider>
<BrowserRouter>
<Switch>
<Route exact component={MainPage} path="/" />
</Switch>
</BrowserRouter>
</AuthProvider>
)
This is not a problem in the library react-router, as we can check using a different provider — context provider in this case. Let’s save a state in a context and add a setter, then the component receives this state from context and changes the text.
And in our component:
import { useState, useEffect } from 'react'
import { useAuthContext } from '../context/AuthContext'
const MainPage = () => {
const [result, setResult] = useState('')
const [isLoading, setIsLoading] = useState(true)
const { isAuthenticated } = useAuthContext()
useEffect(() => {
(async () => {
const result: string = await fakeCall('DONE!')
setResult(result)
setIsLoading(false)
})()
}, [])
return isLoading ? (
<div className="danger" data-testid="Div::Loading">
Loading...
</div>
) : (
<>
<div className="success" data-testid="Div::Result">
{result}
</div>
<div>{isAuthenticated ? 'Welcome!' : 'You Are Not Logged In!'}</div>
</>
)
}
export default MainPage
The problem is about Jest not being able to detect changes in props coming from a parent’s provider.
Not detecting async promises resolve
Most frontends for companies are meant to work with some API, so testing requests to an API is basic. But trying this in CRA has the same result as the other two problems exposed. Let's add fetch (fakeCall) when the component is mounted.
Again, we notice Jest fails. As these are fairly common cases we have to check how to test them. Let us analyze first why this problem happens and then we can check an easy way to solve each of the three cases mentioned.
Why testing async functionality is hard for default CRA libraries
Jest typically expects to execute the tests’ functions synchronously. If we do an asynchronous operation, but we don't let Jest know that it should wait for the test to end, it will give a false positive.
The problems exposed before are just cases of async functionality that face this problem in jest.
How to test async functionality
Testing changes in state
In order to wait for all state changes to consider the component completely rendered, we need to wrap the render with the act() function, which will make the component wait for all asynchronous processes in the component render to be completed, including all state changes that make the component render again.
The result of the component render should be assigned to a variable outside of the scope of the act callback, named component in the case below. This way, once all renders are completed we will have the last version of the component, and then we will be able to check the document to expect certain things to be found on it, as shown below.
If we don’t wait for the component to be completely rendered, we will just get the first mounted version, and maybe not get the expected results. In the example below, we’re looking for an element with the test id attribute Div::Result. We should see this element in the document only when the isLoading state changes to false.
Testing change in props from providers
When the component that we want to test consumes data from a context in the upper level of the app tree, what we need is to mock the return value from that context, as we need to see current component behavior, based on certain values of the context. This can be done by mocking the context module using jest.mock() function.
In the case below, useAuthContext provides us access to the context and all values inside, that’s the function we need to mock to determine later what we need to get returned. Also, note that we’re using jest.requireActual() to tell the mock that everything else that can be used from this module mock — other functions or objects — will stay as the real module.
Before rendering the component inside the act wrapper (explained on the last point), we need to tell the test what we need to get returned when useAuthContext is called in the component. After this, the test can continue by wrapping the render in the act() function and checking the presence of certain things in the document.
Another important thing to note is that if we mock the context module, we don’t need to include it in the component’s render, as the test will understand that AuthProvider has also been changed by a fake provider.
Testing async promises
Usually, we don’t need async API calls to be executed when testing a component, as the act wrapper doesn’t wait for it to be completed. The solution is to mock the service layer module. In the same way as we mock the context module (point 2), we can mock all of the API call functions that are being used in the component. Then, depending on what behavior we need to test, before rendering the component we need to tell the test what we need to get from the resolved (if we want to simulate a successful API call) or rejected (if we want to simulate an error while performing the API call) promise.
The object that we’re mocking should match with the one received from a real call. That way, we can expect normal behavior in the component test. After this, the test can continue by wrapping the render in the act() function and checking the presence of certain things in the document.
Conclusion
The team and contributors behind Create React App have done great work helping us developers to test common and some uncommon behaviors on React apps. As we noted, some common problems found while testing asynchronous code can be tested and we can get great coverage in our unit tests.