Most tutorials on testing in React cover easy cases, but in real life the most difficult test cases are asynchronous request calls to APIs, how components change state, and how those changes affect the view. Let me share my method for testing async API calls using Jest and TypeScript in a CRA app.
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.
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.
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.
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.
Our new code will have some states that would be changed on mounting:
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.
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.
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:
The problem is about Jest not being able to detect changes in props coming from a parent’s provider.
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.
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.
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.
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.
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.
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.
We’d love to learn more about your project.
Engagements start at $75,000.