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.
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
type
attribute or a differenttype
attribute 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
for
and input'sid
are 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
for
andid
are 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
toHaveNoViolations
matcher.Pass container for component into
axe
function 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:
wait
functionThis 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,
wait
usesact
, 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
mockLoadGreeting
just to make it clear what it's doing.mockResolvedValueOnce
functionTakes the argument data and makes it the resolved value of the promise that
mockLoadGreeting
returns whenGreetingLoader
invokes 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.fn
and 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
onUnhandledRequest
so that all requests other than the ones we mocked will error out.Like
jest.clearAllMocks
,server.resetHandlers
clears 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:
CSSTransition
causes 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
in
istrue
, display the message.If
in
isfalse
, 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.error
messagesTest 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
getBy
orqueryBy
orfindBy
, andReturn any of the above for use in your tests.
Example:
Test components that use react-router Router
provider and createMemoryHistory
Router
provider and createMemoryHistory
Some 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
history
object,Ability to pass render options into React Testing Library's native
render
function, andAbility to use
rerender
and still automatically wrap the component in theRouter
provider.
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
initialState
andstore
, 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
Wrapper
function component that does theProvider
wrapping for us and then pass it in options forrtlRender
. That way if we use thererender
util, theProvider
will always wrap around the component in the future. (We don't have to do it ourselves.)We return the
store
itself 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
renderHook
It 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
rerender
If 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
within
React 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