Test React Components With Jest And React Testing Library
This section shows you the principles and practices to test how you're putting your DOM on the screen. The goal is to give you the confidence to ship your app with peace of mind. We'll be focusing on React, Jest, and React Testing Library, but these ideas can apply to any framework.
Basic React Testing
We can actually test our React components using ReactDOM.render directly. This is a solution that doesn't depend on more testing libraries.
Create a
div.Renders your React component inside the
div.Use DOM query methods and node properties to test the DOM elements rendered.
import React from 'react';
import ReactDOM from 'react-dom';
import TextInput from './TextInput';
test('renders label and input', () => {
const labelText = 'Input Text Here';
// 1.
const div = document.createElement('div');
// 2.
ReactDOM.render(<TextInput label={labelText} />, div);
// 3.
expect(div.querySelector('label').textContent).toBe(labelText);
expect(div.querySelector('input').type).toBe('text');
});Adding Jest DOM library
The trouble with our naive solution above using basic Jest and ReactDOM.render is that our error messages during testing are not that semantically useful.
The way to get clearer error messages is to extend the possible Jest assertions by introducing Jest DOM. This library adds custom test matchers specifically used for DOM testing.
Now our test is more useful:
If the DOM node doesn't exist, you'll receive a meaningful error message telling you.
If the element doesn't have a
typeattribute or a differenttypeattribute thannumber, you'll also hear about it.
Pro tip: It's irritating to have to manually extend every custom matcher. We can solve this problem by importing the following:
(You can even configure your project to have Jest automatically import the extension before running any test.)
Adding DOM testing library
In its current form, our test has a few issues:
We are searching specifically for
'Favorite Number'as the label text, making our test tightly coupled to implementation details. The user doesn't care about letter case and exact wording.If the label's
forand input'sidare different, this will break the application for screen readers, yet the test will pass.
To solve this, DOM testing library introduces some neat approaches to testing the DOM:
Refactor 1: programmatically associating label and input
We will be using queries.getByLabelText to enforce a connection between the label and input.
What's happening here is that DOM testing library is searching for a label by text, finding the input associated to it, and returning that input.
This streamlines 2 things:
We're automatically testing that
forandidare the same.By finding by label text, we're automatically testing that a label with the given text exists.
Refactor 2: regex
The user doesn't care that our label text is exactly 'Favorite Number', so our tests shouldn't either. We can actually pass regex to solve this problem:
Now we capture the essentials of what the user wants without getting as bogged down in the implementation.
Refactor 3: node-specific queries
It's awkward the passing parent element div into every query we use. Instead, we can pass our parent element once and destructure all the queries we need.
React Testing Library
This subsection introduces React testing library and much of its useful functionality.
Render function
In our example above, our steps to render our component can be abstracted for reuse:
This is essentially what render from React testing library does. Now in every component test, we can just go:
Writing tests with debug
When writing your tests with React testing library, you have access to a function called debug that prints out the DOM you're testing. This is useful in the same way console.log is useful.
To access, debug, simply destructure it:
Testing event handlers
React testing library introduces the fireEvent function to simulate events and trigger props like onChange or onClick in your components.
Pro tip: The trouble with using fireEvent in isolation is that it doesn't perfectly mirror user behaviour. For example, when a user types text into an input element, it's not only a change event that fires. There are many events that fire: focus, key up, key down, change, etc.
The good news is that there is a user object that you can use to simulate user behaviour. This user object basically just uses fireEvent behind the scenes but fires many different events at once.
Updating props with rerender
Sometimes you want to test a component after a props update. To do this, we use rerender. rerender basically re-renders your component in the exact same container, allowing you to pass in new props in the process.
Asserting that something is NOT rendered
There's actually a problem with our code above: it will throw an error when you call getByText(/number is invalid/i). That's because no error message is displayed after our re-render, so our getByText will tell us that it didn't find anything by throwing an error.
The solution is to use queryByText. Any query method doesn't throw an error when it doesn't find anything. Instead, it returns null.
Pro tip: Generally, we want to use get methods because they throw useful error messages. If, however, we need to test that something is not rendered, then query is your best bet.
Testing accessibility with jest-axe
jest-axe extends Jest, allowing us to make assertions about the accessibility of your components. The process to test accessibility goes like this:
Extend Jest with custom
toHaveNoViolationsmatcher.Pass container for component into
axefunction to asynchronously return results of axe analysis.Make assertion to test that the results don't have any violations.
Now if there are accessibility violations, we will receive informative, descriptive error messages. These error messages will tell us exactly what isn't accessible, how to fix it, and how to learn more.
Pro tip: We can automatically extend Jest to use jest-axe matchers using import 'jest-axe/extend-expect'.
Mocking HTTP requests with jest.mock
Suppose we have a component that asks to submit a person's name and returns a custom greeting text queried from an API.
This is how we would test it:
Mock the function that queries the API, telling the mock function to return a fake promise with fake data.
Render the component and destructure your required queries.
Insert a name into input.
Click button to submit name.
First: Test that the mock function was called in the right way (with the correct arguments and the correct number of times).
Wait for the fake promise to resolve by waiting for the greeting text to display in the DOM.
There's a few things to note in our test:
waitfunctionThis is an async function that continually calls its callback until it doesn't throw an error anymore.
We usually insert an assertion that only passes after the promise resolves and updates the DOM.
Note: Behind the scenes,
waitusesact, a function that ensures the render updates inside the component have been processed before making your assertions.It also has a timeout value that will throw a final error, implying that the assertion inside will never pass.
Pro tip: We rename the mock function into
mockLoadGreetingjust to make it clear what it's doing.mockResolvedValueOncefunctionTakes the argument data and makes it the resolved value of the promise that
mockLoadGreetingreturns whenGreetingLoaderinvokes it.
Mocking HTTP requests with dependency injection
Some environments don't support jest.mock. For example, Storybook doesn't support it. In this case, here's an alternative mocking approach called dependency injection.
In your component, add a prop that accepts the real version of the function as a default value.
In your test, create a new
jest.fnand pass it to the component as a prop.
With this coding pattern, the component doesn't need to be passed the function all the time but only in the test.
Mocking HTTP requests by intercepting them
Instead of mocking out the functions that make the API call, we can allow our functions to behave normally but then ** intercept** the requests.
To do this, we need the msw package:
Notes:
We include
onUnhandledRequestso that all requests other than the ones we mocked will error out.Like
jest.clearAllMocks,server.resetHandlersclears out any handlers we created in specific tests.
Mocking animations with jest.mock
Testing a component with an animation could work by using async/await and the wait function. However, this still takes time, which is bad for our tests if we want to test the functionality of our tests instantly.
To be able to test our components instantly without waiting for our animations to complete, we can mock out the animation libraries we use. For example, here's how we mock out react-transition-group:
Here's what's happening in the code above:
CSSTransitioncauses an element to fade in and out, each time taking 1 second to complete the animation.To shorten this process, we mock out
CSSTransition, turning it into a functional component that does the same thing but instantly.If the
inistrue, display the message.If
inisfalse, don't display it.
Pro tip: Be cautious when you mock your functions. They should capture the essence of what the real function is trying to do.
Testing error boundary component
There are a few things we usually want to test in any error boundary component:
Test that the error is reported to some logging service
Hide any unnecessary
console.errormessagesTest that you can successfully recover from error
To test if an error is reported, we mock the report function:
We don't want our tests to get cluttered up with console.error messages, which are generated automatically when an error is thrown. To fix this, we can mock console.error:
To test that we can recover from an error, we will test that the content from the error boundary displays and can be interacted with. For example, maybe we have a "Try Again" button that re-renders the component:
Simplifying rerender with wrapper option
The render function has a helpful options object that you can pass where you provide a wrapper to simplify your rerender calls.
So instead of this:
We can do this:
Mocking components
In our example, we can test that the Redirect component from react-router renders correctly. This component is used to redirect to another page upon render.
Note: We expect a 2nd argument to be passed because that's React automatically passing a context.
Testing dates in React
Sometimes we want to store a date like when we are writing data to a database. On the frontend, we would simply create a new Date(). However, this becomes an issue in testing, as our mock payload will have its own new Date(), which may be off by milliseconds. So this would fail:
To solve this, we can test for a time range instead:
Custom render function to share between tests
Sometimes our tests share the same initial code. In these cases, it's reasonable to create a custom render function for that repeated code.
Things you can do in a custom render function include:
Set up the mock data you need,
Render the component itself,
Perform the initial interaction with the component that you need,
Get elements in the component using
getByorqueryByorfindBy, andReturn any of the above for use in your tests.
Example:
Test components that use react-router Router provider and createMemoryHistory
Router provider and createMemoryHistorySome components you test will have react-router components:
If you test the Main component on its own, it will error out because it must be wrapped in a Router provider. To test with this provider, you just need to wrap your component in the provider inside your render function.
Pro tip: You use Router and createMemoryHistory when you want to customize the user's history. When you don't need to customize, it's recommended to just use BrowserRouter from react-router-dom.
Test 404 route by passing a bad entry in history object
history objectIn our routes, we had a FourOhFour route that acts as a fallback if no matching path is found.
To test this, we can just pass a bad entry into createMemoryHistory:
Now the FourOhFour component should render.
Custom render function for testing react-router components
Our custom render function will have the following features:
Ability to include custom initial route,
Ability to pass custom
historyobject,Ability to pass render options into React Testing Library's native
renderfunction, andAbility to use
rerenderand still automatically wrap the component in theRouterprovider.
Testing a redux-connected component
Suppose you have a component that is connected to Redux:
All you need to do to test this component is wrap it in a Provider with store passed as a prop.
The magic of this approach is that
You're testing the component and redux at the same time, since they're integrated together, and
You're testing the component as if redux doesn't exist (it's just an implementation detail), so if you ever stop using redux, it's not too hard to refactor it out of your tests.
Testing a redux-connected component with initialized state
The key to passing initial state is to run your createStore in your tests:
Custom render function for redux-connected components
If you're testing redux-connected components, you are going to be wrapping your component in a Provider and passing a store with a custom initialized state a lot. We can simplify this with a custom render function.
There's a few things to unpack in this custom render function:
We destructure out
initialStateandstore, so we can pass the options specific to React Testing Library intortlRender.We pass a default initialization for
store, so a user can pass their own custom store if they want.We create a
Wrapperfunction component that does theProviderwrapping for us and then pass it in options forrtlRender. That way if we use thererenderutil, theProviderwill always wrap around the component in the future. (We don't have to do it ourselves.)We return the
storeitself in case we want some fine-tuned control of redux.
Test custom React hook
If you use a React hook in isolation, you'll get an error stating that it must be called inside a function component. To circumvent this, we just call the hook inside a function component!
Our result has a count state and increment and decrement state updaters. The trouble now though is that we can't just invoke a state updater. We always have to wrap it in a callback in act.
Setup function to test custom React hooks
If our custom React hook accepts arguments to customize its behaviour, we may want to create a custom setup function that handles all the setup required to test each scenario.
Note: Notice how we store the return value of useCounter in result.current? That's because if we store it directly in result and then return result, we only get back the initial state of useCounter. On every re-render , useCounter returns something new, so to access that new value every time, we store it inside an object.
Test custom React hook with renderHook
renderHookIt turns out that React Testing Library already provides you with a helper function for testing hooks: renderHook. Just pass your hook into it, and you have access to the hook's internal state using result.current.
Test updates to React hooks with rerender
rerenderIf you want to change the props passed to your custom hook, you can use the rerender util that gets returned from renderHook.
Test React portals with within
withinReact portals are unique ways to render a component outside of its parent.
Suppose you have this Modal wrapper that creates a React portal for its children.
How do you test for a component that appears outside a parent?
Answer: By default, the render test function returns test utils that are bound to the entire document.body. That means you don't have to do anything special to test your portaled component!
If however we want to bind our test utils to a custom wrapper element (instead of document.body), we can use the within function:
Or we can just pass the modalRoot as the baseElement inside render!
Test unmounting a React component
Suppose you have a component that uses setInterval to periodically update state. When this component unmounts then, you need to clearInterval because you can't have the interval continuing to try to update state of an unmounted component. In fact, React will throw an error if you do.
To test that cleanup occurs when you unmount, you can use the unmount util:
Writing integration tests
Suppose we have a multi-page form where each step takes you to a new component/page. How do we test this?
With React Testing Library, we can perform an integration test where we imitate what a user does:
Improve reliability of integration tests using find queries
find queriesWith integration tests especially, there can be many variables that can break your tests in unexpected ways. For example, what if there are animations that slow down rendering of certain elements?
To help separate the implementation details from your tests, it's a good idea to use find queries, so your tests don't automatically break if something doesn't immediately go as planned.
Improve reliability of integration tests using User Event Module
To help even further separate the implementation details from our tests, we can use the user-event module inside React Testing Library.
This module gives us user actions that more closely represent how a user actually behaves. This is a much more accurate approach to testing than imperatively running fireEvent methods.
For example, instead of fireEvent.click, we can use user.click. Or instead of fireEvent.change, we can use user.type.
Last updated