Testing a React app with React Testing Library (RTL)

Lucas Quinteiro
White Prompt Blog
Published in
9 min readMay 16, 2022

--

The Importance of Unit Testing

In the world of software development, unit testing plays a vital role with ensuring that only the best products reach the end user. Unit testing involves testing the individual components of an application to confirm that each piece is functioning well for the entire system.

At White Prompt, testing is integrated into our process as we integrate software development and IT operations teams to build, test, and release software at lightning speed. From coding and deployment to maintenance and updates, our engineers create the tools and methodologies to bring teams closer through collaborative product development. Build your next project with us by starting with a free consultation.

In this blog post, we will discuss how to completely test the functionality of a React App using Next.js. The goal is to walk through each step of testing, highlighting different case scenarios and showing various approaches to test them. Want to learn more about Next.js and how this React framework functions? Check out the site to access documentation and walk through how to effectively use it.

Frequently when deadlines are tight, frontend unit testing is set aside until the end or never completed. In some cases, even when there is an intention of doing frontend unit testing, the team is discouraged due to the difficulty to understand testing errors that are not related to the functionality of the application. Instead, they are caused from the way the unit tests need to be implemented.

Using the React Testing Library

The React Testing Library is a great resource to employ when unit testing a component’s functionality. This detailed documentation serves to help with testing React components, however, it also has a learning curve when testing complex web apps and can result in unit testing abandonment.

Index

A web application was built to demonstrate using React Testing Library to review the following subjects:

  • Testing component’s behavior and emulating user events to assess for the expected result.
  • Mocking API calls with msw library.
  • Mocking custom and node_modules components imports in case they should not be included in our tests.
  • Accessing to env variables in our tests.
  • Troubleshooting some common errors we discovered while testing.

Our App

We will be testing a very simple web application built in Next.js that will hit a mocked API (we will intercept requests with msw also in the browser).

The scope includes:

  • Sign In page.
  • Register page.
  • Users list page.
  • User details page.

We will also have a HOC (high order component) that wraps each page to decide if the user can access or not depending on if he/she is authenticated. Let’s get started!

Testing the component’s behavior

The React Testing Library aims to test the component’s behavior by accessing directly to the DOM element. To note, this component’s behavior is trying to emulate the interactions within the app.

We will start by testing the “Sign In” page with its corresponding form. Basically, we want to make sure that:

  • all our inputs are shown;
  • errors are shown correctly when inputted data is invalid;
  • how the form reacts to asynchronous errors (for example, invalid credentials);
  • and how the page behaves when the form is submitted correctly.

To access each element from the DOM, the React Testing Library has three main methods with varieties in each (from testing library docs):

  • getBy…: Returns the matching node for a query, and throws a descriptive error if no elements match or if more than one match is found (use getAllBy instead if more than one element is expected).
  • queryBy…: Returns the matching node for a query, and returns null if no elements match. This is useful for asserting an element that is not present. Throws an error if more than one match is found (use queryAllBy instead if this is OK).
  • findBy…: Returns a promise which resolves when an element is found that matches the given query. The promise is rejected if no element is found or if more than one element is found after a default timeout of 1,000ms. If you need to find more than one element, use findAllBy.
https://github.com/lucasquinteiro/react-testing-demo-app/blob/main/tests/login.test.tsx

In this case, as we are using queryByTestId, it will return null if the element is not rendered or return the element if it is. So, we can expect the element to be truthy in order to validate that the input is there (the test will not fail by itself if we don’t expect these inputs to be truthy).

Another approach we can take is to use getByTestId. This method also returns the element, but it makes the test fail immediately with a log in the console describing what’s being rendered when it cannot find the element. This way we don’t need to expect the input to be truthy.

https://github.com/lucasquinteiro/react-testing-demo-app/blob/main/tests/login.test.tsx

As you can see, we are accessing our components using byTestId. To be able to identify an element by test id, we first need to add the data-testid prop in the component that we want to access. There are many other ways to access components which we will discuss later on in the post (*byText, *byRole, etc.). Here is how we set the test id for our text fields (also, we will be using formik to handle our forms):

https://github.com/lucasquinteiro/react-testing-demo-app/blob/main/components/Inputs/TextField/index.tsx

User Events

Now, let’s say we want to test if the errors are shown correctly when we input an invalid email format or a password that is too short. To emulate user actions, such as typing into an input or clicking a button, we can use the userEvent method from the React Testing Library.

NOTE: we are passing a {delay: 0.5} to the typing actions. This will make the test more sequential (which will be nearer to a real user experience).

https://github.com/lucasquinteiro/react-testing-demo-app/blob/main/tests/login.test.tsx

In this case, we are using findBy* method to access the error labels. This is very useful because the errors will only be shown based on the input from the user. As there will not be any errors on the first render, error labels will not be available. The findBy* queries try multiple times to find the elements and if it cannot find it after a timeout, it fails with an error similar to the getBy* query. We can use this method when we are waiting for some element to appear depending on an async event.

Something else to notice is that we are using the findByText accessor. Personally, we prefer to use findByTestId and then verify if each element contains the text we are expecting, but this is another completely valid way of assessing it.

Mocking API calls with msw

In order to test the whole behavior of our app, we want to be sure that it works as expected depending on what the API requests return. Obviously, we will not be hitting the real API because we are only testing the frontend, so we will need to mock the responses. A great library to achieve this is msw. This library intercepts API calls and lets you handle each API call to return whatever response you need in each situation, for example 400 errors, 200 successful, etc.

In order to setup msw, we need to create the following files:

  • msw-server.ts: Here we setup the server that receives the handlers array.
https://github.com/lucasquinteiro/react-testing-demo-app/blob/main/tests/mocks/msw-server.ts
  • handlers/auth.ts: Here we define the handlers for all the auth related routes.
https://github.com/lucasquinteiro/react-testing-demo-app/blob/main/tests/mocks/handlers/auth.ts
  • handlers/index.ts: we export all the handlers.
https://github.com/lucasquinteiro/react-testing-demo-app/blob/main/tests/mocks/handlers/index.ts
  • jest setup file: In your setup file you need to initialize the server beforeAll tests and reset the server handlers afterEach test.
https://github.com/lucasquinteiro/react-testing-demo-app/blob/main/tests/setup/index.ts

Notice that in handlers.ts we mocked the POST request to the login endpoint returning a 200 status and some mock response. But let’s say we want to test how the “Sign In” page behaves when the request fails (for example, wrong credentials). In this case, the server.use method lets us override the handler for this test to define an error response only once.

https://github.com/lucasquinteiro/react-testing-demo-app/blob/main/tests/login.test.tsx

Mocking custom and node_modules components

By overriding the export

Let’s say we are testing a page that includes a very complex component that we don’t want included in our test. It could also be that this component is causing errors in our test that are not related to the behavior of the application, but they are caused due to an incompatibility of the component with the testing library. Another scenario could be that you just want to make your test simpler and you don’t want to include a component. As you can see, there are enough case scenarios to mock a component in your tests.

A good usage of mocking modules in our demo application would be to mock the next/router module. For example, when testing the component for “User Details” page we will be reading the user id from the route. As we are just rendering the component returned by the page (and not the whole next.js page), all the routing will not be available in our test. In this case, it’s useful to mock the useRouter function from the next/router module, so that it will return the route for our page.

As we will probably want to mock the useRouter function in every test, we can place the mock in our jest setup file (remember that this file will run before all tests).

https://github.com/lucasquinteiro/react-testing-demo-app/blob/main/tests/setup/index.ts

What we are doing here is replacing what the next/router module will export. In our case, we only need to mock the useRouter function, so we will replace it with a jest.fn(). Now that the useRouter is a jest.mock function, we can easily handle what we need that function to return using the mockReturnValue function. Here is an example:

https://github.com/lucasquinteiro/react-testing-demo-app/blob/main/tests/user-detail.test.tsx

By using a __mocks__ folder

Another way of mocking components is by creating mock files. Let’s say we now want to mock our Navbar component. We can do this by simply creating a __mocks__ folder at the same level as the index.tsx file that exports the Navbar and creating a file with the same name in the __mocks__ folder like this:

__mocks__ folder

Then, we just call jest.mock where we want to apply the mocking and it’s done!

In this case, we can place the jest.mock in our setup file so that it will be mocked in all of the tests.

https://github.com/lucasquinteiro/react-testing-demo-app/blob/main/tests/setup/index.ts

Using env variables in tests

In order to access env variables in the testing environment you need to:

  • Place your variables in a .env.test.local file.
  • Install @next/env (in the case you are using Next.js).
  • Create a setupEnv.ts file (you can call it whatever you want).
https://github.com/lucasquinteiro/react-testing-demo-app/blob/main/tests/setup/setupEnv.ts
  • Add the globalSetup attribute to the jest.config.js file pointing to setupEnv.ts.
https://github.com/lucasquinteiro/react-testing-demo-app/blob/main/jest.config.js

Troubleshooting some common errors

Overlapping act() calls

Let’s say we want to test our “Users” page and make sure that all of our users fetched from the API are being shown correctly. We could iterate through the array of users and validate that the name is rendered. The test will look like this:

https://github.com/lucasquinteiro/react-testing-demo-app/blob/main/tests/users.test.tsx

We can see that the test is passing but it throws these warnings:

Warning: You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one.

This warning is notifying that you are trying to do many things at the same time (overlapping act() calls). Remember that when calling user actions or any assertion in our tests, we want to emulate what a user might do. Obviously with our testing, it shouldn’t be possible to do multiple things at the same time (for example, typing into two inputs, or clicking a button while doing something else, etc.).

In our tests, we are iterating through the users using .map method, which doesn’t run sequentially. An easy fix would be using .mapSeries method from the bluebird library which is basically a sequential .map method.

https://github.com/lucasquinteiro/react-testing-demo-app/blob/main/tests/users.test.tsx

Now lets run the test again, and we can see that the warning is gone:

Conclusion

Testing the functionality of our application is very important to make sure it works consistently as it grows and new bugs aren’t introduced into parts of our app that we thought were functioning properly (in other words, we can prevent regressions). Also, the ideal development approach would be combining this with TDD (Test Driven Development).

Demo App repo: https://github.com/lucasquinteiro/react-testing-demo-app

We think and we do at White Prompt.

Are you ready to build your custom project for your business? Looking for a nearshore solution to get far-reaching results?

Let’s talk. We can help you to make it happen!

Further Reading

--

--