There is not that one single tool to test React Components. React Testing Library seems to be the recommended one, but in the end it is actually up to you and which style of testing you prefer.
Overview of available testing tools
Jest is a JavaScript test runner that lets you access the DOM via jsdom
, which is a lightweight browser implementation that runs inside Node.js. Using a virtual DOM browser has advantages over a real browser:
- It runs quicker than having to start up a browser for each test
- It runs in the same process as your tests, so you can write code to examine and assert on the rendered DOM
- It behaves like a regular browser would, but doesn’t have features like layout and navigation (you can use mocha instead)
There are other test runners like mocha or ava, but in this article we focus on Jest. If you use Create React App, Jest is already included out of the box with useful defaults.
React Testing Library (package @testing-library/react
) is
- a set of helpers that allow you to write tests that focus on the component’s functionality instead of the specific way it was implemented. That’s good because a component’s implementation may change during refactoring, but the underlying functionality should not (in most cases).
- You query DOM nodes instead of having to deal with instances of Components
- is built on top of
react-dom
andreact-dom/test-utils
- is similar to AirBnb’s testing library Enzyme.
Direct Testing: You can also choose to test a component directly without using any special tools. But it doesn’t offer so much convenience, that’s why we won’t go into details in this article.
Test Utilities (package react-dom/test-utils
) which is a set of useful functions to test components. The official React Docs recommend that you should use React Testing Library instead. That’s the reason why we won’t focus too much on Test Utilities in this article.
Test Renderer (package react-test-renderer
) sits between ‘direct testing’ and React Testing library because it allows you to test against React abstractions such as Components and props instead of HTML DOM elements and thus does not use a browser or virtual DOM.
Frameworks like Cypress, puppeteer and webdriver are useful for running end-to-end tests.
Rules to make testing easier
- Try to put as little business logic in components as possible. Instead put the logic in service files and test those separately.
- Focus on testing functionality, not specific implementations: For example, in your tests you should rather query for an element with a label “Load data” instead for a specific anchor-element. Then your test still works if you later decided to change the anchor-element to a button-element, because the text “Load data” is the same.
React testing library
npm install --save-dev @testing-library/react yarn add @testing-library/react
The basic procedure is:
render(<MyComponent />)
- Think about what a user would do in a real life scenario to test a particular component in a browser. Would the user press a button to test the functionality? Then query for the button by using one of the many query-methods (see below), for example
getText('Load')
to get an element with textContent'Load'
. - You can fire events on the element, like a user would, for example
fireEvent.click(myButton)
- You wait for page updates and use either the container that the render function returns or – preferably – the
screen
type which queriesdocument.body
. - You verify the result with what you
expect()
import { render, fireEvent, screen } from '@testing-library/react' test('loads items eventually', async () => { render(<Page />) // Click button fireEvent.click(getByText('Load')) // Wait for page to update with query text const items = await screen.findAllByText(/Item #[0-9]: /) expect(items).toHaveLength(10) })
Querying for an element
As mentioned before: It should not be important to test in which hierarchical position an element is rendered, so instead of drilling down the element hierarchy or querying by CSS classes you get a bunch of useful query functions that let you get to the elements:
Here are all query suffixes:
Role | find by aria role (prefered way) | screen.getByRole('button', {name: /submit/i} Overview of all roles. |
LabelText | find by label or aria-label text content | screen.getByLabelText('Username') |
PlaceholderText | find by input placeholder value | |
Text | find by element text content | Exact matchscreen.getByText('llo Worl', {exact: false}) Non-Exact match screen.getByText('llo Worl', {exact: false}) RegEx screen.getByText(/world/i) Custom function screen.getByText((content, element) => content.startsWith('Hello')) |
DisplayValue | find by form element current value | |
AltText | find by img alt attribute | |
Title | find by title attribute or svg title tag | |
TestId | find by data-testid attribute |
Before matching against text it gets normalized, that means trimming whitespace from the start and end of text, and collapsing multiple adjacent whitespace characters into a single space. You can prevent/customize this by providing a custom normalizer function:
screen.getByText('text', { normalizer: getDefaultNormalizer({trim: false}), })
Screen
All of the queries exported by DOM Testing Library accept a container
as the first argument, unless you use screen (recommended) which queries document.body
. You need a global DOM environment to use screen
. If you’re using jest, with the testEnvironment set to jsdom
, a global DOM environment will be available for you.
You must combine one of the suffixes from above with one of the following prefixes. For example LabelText and getAllBy results in getAllByLabelText
(“Load”) which will get all elements if the label or aria-label text content is Load.
No Match | 1 Match | 1+ Matches | Retry via async/await? | |
---|---|---|---|---|
Single element | ||||
getBy | throw | return | throw | No |
findBy | throw | return | throw | Yes |
queryBy | null | return | throw | No |
Multiple elements | ||||
getAllBy | throw | array | array | No |
findAllBy | throw | array | array | Yes |
queryAllBy | [] | array | array | No |
There is a very cool Browser extension for Chrome and Firefox named Testing Playground, and it helps you find the best queries to select elements
Some examples
So a simple test could look like this:
it('should render a container', () => { // 'container' is a reference to the DOM node where // the component is mounted const {container} = testLibRenderer(<Footer />); expect(container).toBeDefined(); expect(container.outerHTML).toBe('<div>The footer</div>'); });
By default, React Testing Library
will create a div
and append that div
to the document.body
and this is where your React component will be rendered. But you can change that if you need to.
// return form field by label r.getByLabelText('First Name'); // return a text element by its content r.getByText('My text'); // query elements by title r.getByTitle('myTitle'); // if all else fails you can query for elements that had a 'data-testid' attributed assigned to them previously by you r.getByTestId('my-test-id');
import { render as testLibRenderer, fireEvent, screen } from '@testing-library/react' describe('version history', () => { it('should display the version history', () => { const { getByTestId } = testLibRenderer(<Footer />); const versionLink = getByTestId('footer-test-id); expect(versionLink.textContent).toMatch(/Version number.*/); }); });
Query for hidden or checked elements
Use query options to match for elements that are hidden, checked, selected etc.:
getByRole( // If you're using `screen`, then skip the container argument: container: HTMLElement, role: TextMatch, options?: { exact?: boolean = true, hidden?: boolean = false, name?: TextMatch, normalizer?: NormalizerFn, selected?: boolean, checked?: boolean, pressed?: boolean, current?: boolean | string, expanded?: boolean, queryFallbacks?: boolean, level?: number, }): HTMLElement
Using a matcher function
Let’s say you have a breadcrumb component that looks like levelA > levelB > levelC
, but the DOM structure looks like:
<body> <div> <div> <span> <a class="sc-bdVaJa bTOuaX" href="https://www.test1.local" target="_blank" > levelA </a> > </span> <span> <a class="sc-bdVaJa bTOuaX" href="https://www.test2.local" target="_blank" > levelB </a> > </span> <span> <a class="sc-bdVaJa bTOuaX" href="https://www.test3.local" target="_blank" > levelC </a> </span> </div> </div> </body>
Now, if you would want to query the breadcrumb with
screen.getByText('levelA > levelB > levelC');
then you get:
TestingLibraryElementError: Unable to find an element with the text: levelA > levelB > levelC. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Yes, the query does not work because the text is broken up by multiple elements. The solution here is to use a matcher function like this:
screen.getByText('levelA > levelB > levelC'); const result = screen.getByText((content, node) => { const hasText = (node) => node.textContent === "levelA > levelB > levelC"; const nodeHasText = hasText(node); const childrenDontHaveText = Array.from(node.children).every( (child) => !hasText(child) ); return nodeHasText && childrenDontHaveText; });
The matcher function is invoked for every node and returns true if it matches our criterium, which is: Does the node or any of its children contain levelA > levelB > levelC
?
Testing component events
What it boils down to – when testing events – is to use fireEvent
, for example fireEvent.click(myButton)
.
In the following example you have a Footer
that – when clicked on a link – renders another Component VersionHistory
. The way to test whether the click worked and actually displayed the VersionHistory
is to first fire the click event and then check for the presence of the VersionHistory
.
// snippet of Footer.jsx import * as React from 'react'; import styled from 'styled-components'; export default () => { const [showVersionHistory, setShowVersionHistory] = React.useState(false); const onClickButton = () => { setShowVersionHistory(!showVersionHistory); } return ( <StyledFooter> {showVersionHistory && <VersionHistory showVersionHistory={showVersionHistory} setShowVersionHistory={setShowVersionHistory}/>} /* Here is the link to test */ <a onClick={onClickButton} data-testid="version-history-link" </a> ); }
// snippet of VersionHistory.jsx return <div className="version-history"> <h1>Version History</h1> </div>;
You test Footer
‘s click behavior in three steps:
it("renders version history after clicked on first link", () => { // 1. Render the component const { container } = testRender(<Footer/>); // 2. Simulate a click event on the link fireEvent.click(screen.getByTestId("version-history-link")); // 3. check that the output has a h1 with the expected content expect(container.querySelector('h1').textContent).toBe('Version History'); });
With another test you can test the toggle behavior of the link:
it("toggles version history", () => { const { container } = testRender(<Footer/>); fireEvent.click(screen.getByTestId("version-history-link")); expect(container.querySelector('h1').textContent).toBe('Version History'); fireEvent.click(screen.getByTestId("version-history-link")); expect(container.querySelector('h1')).toBeNull(); });
Asynchronous tests
Just wrap your asynchronous expected code into a wait() callback. This also works when testing a component that uses useEffect hook to load things asynchronously:
it('should do something', async () => { // ... // Let's assume the click calls an async function fireEvent.click(screen.getByTestId("version-history-link")); await wait(() => { expect(...).toBe(...); }); })
Testing Event Mocks
Mocks in Jest
That’s how you create a mock in Jest that is able to record how many times it has been called and which arguments have been passed in:
const f = jest.fn();
That’s how you test if the mock was called with specific parameters:
f(1,2,3); f('a'); expect(f.mock.calls).toEqual( [ [1,2,3], ['a'] ] );
Here is how you give a mock a specific implementation:
const f = jest.fn((a, b) => a + b); expect(f(3,7)).toBe(10); expect(f.mock.results).toEqual([{ type: "return", value: 10 }]);
You can also hard-code a return value instead:
const f = jest.fn(); f.mockReturnValueOnce(12).mockReturnValue(0); expect(f()).toBe(12); expect(f()).toBe(0); expect(f()).toBe(0);
Mocking an event triggering Component
Let’s assume you have a slider component that fires an event when the slider is moved. You catch these events by providing a function to onSomethingChanged
.
it('should do something', () => { // 1. We use a jest mock as event handler const fn = jest.fn(); const { getByTestId } = render(<MyComponent onSomethingChanged={fn} />); // 2. We get the input that triggers the event when changed const input = getByTestId("date-slider"); // 3. We manually fire the change event fireEvent.change(input, { target: { value: '3877' }}); // 4. We expect the mock to have been called with specific arguments expect(fn.mock.calls).toEqual([3877]); });
Testing with Test Utils
React provides a helper called act()
that makes sure all updates related to tasks like rendering, user events, or data fetching have been processed and applied to the DOM before you make any assertions.
// hello.js import React from "react"; export default function Hello(props) { if (props.name) { return <h1>Hello, {props.name}!</h1>; } else { return <span>Hey, stranger</span>; } }
Setting up tests
import React from "react"; import { render, unmountComponentAtNode } from "react-dom"; let container = null; beforeEach(() => { // setup a DOM element as a render target container = document.createElement("div"); document.body.appendChild(container); }); afterEach(() => { // cleanup on exiting unmountComponentAtNode(container); container.remove(); container = null; });
// hello.test.js import { act } from "react-dom/test-utils"; import Hello from "./hello"; // ... set up code from above it("renders with or without a name", () => { act(() => { render(<Hello />, container); }); expect(container.textContent).toBe("Hey, stranger"); act(() => { render(<Hello name="Jenny" />, container); }); expect(container.textContent).toBe("Hello, Jenny!"); act(() => { render(<Hello name="Margaret" />, container); }); expect(container.textContent).toBe("Hello, Margaret!"); });
Firing events
Note that container
must be attached to document so events work correctly (see setup above).
act(() => { button.dispatchEvent(new MouseEvent('click', {bubbles: true})); });
Mocking data fetching
If your code under test has
async function fetchUserData(id) { const response = await fetch("/" + id); setUser(await response.json()); }
then you can mock this in your test like that:
const fakeUser = { name: "Joni Baez", age: "32", address: "123, Charming Avenue" }; jest.spyOn(global, "fetch").mockImplementation(() => Promise.resolve({ json: () => Promise.resolve(fakeUser) }) );
and ensure that at the end of the test
// remove the mock to ensure tests are completely isolated global.fetch.mockRestore();
Mocking a module
You want to jest.mock('./SomeThirdPartyComponent')
if your Component under test is depending on it, but the module shall not be tested explicitly.
Let’s say you have a component that is dependent on a service that fetches data. To mock that service you do this:
import myService from './MyService'; jest.mock('./MyService'); myService.someMethod.mockResolvedValue('I now return this');
Of course there are more mock functions:
myService.someMethod.mockImplementation((a, b) => a + b); myService.someMethod.mockReturnValue(7);
Testing timer functions
First jest.useFakeTimers()
and then use jest.advanceTimersByTime(x)
to artificially set the progressed time.
jest.useFakeTimers(); it("should select null after timing out", () => { const onSelect = jest.fn(); act(() => { render(<Card onSelect={onSelect} />, container); }); // move ahead in time by 100ms act(() => { jest.advanceTimersByTime(100); }); expect(onSelect).not.toHaveBeenCalled(); // and then move ahead by 5 seconds act(() => { jest.advanceTimersByTime(5000); }); expect(onSelect).toHaveBeenCalledWith(null); });
Unmounting a Component in a test
act(() => { render(null, container); });
Snapshot testing
Snapshots are a feature of Jest and help to detect unwanted changes between test runs. A snapshot of the component output is created on the very first run of a test. On subsequent test runs another component output is created and tested against the previous one. If they mismatch, the test fails.
Jest either stores snapshots inline (in the test file itself) or as a file in a folder __snapshots__
. Snapshots are supposed to be checked in your versioning system along with the source code.
Unit test can show bugs but they can’t show correctness. Snapshot tests can only detect change, but can’t show bugs nor correctness. That means, that if a snapshot fails that does not tell you that you have a problem in your application. So you should only use Snapshots sparingly and preferably on those components that do not change frequently.
Here is how you write a snapshot test:
it('should do something', () => { const tree = TestRenderer.create(<MyComponent />).toJSON(); expect(tree).toMatchSnapshot(); });
This test cannot fail on the first test run, because there is no preexisting snapshot.
Testing with Test Renderer
Test Renderer renders React Components to pure JavaScript objects without depending on the DOM and allows us to test against React abstractions (like Components and props) instead of HTML elements. That’s why you can test without using a virtual DOM library like jsdom.
// ES6 import TestRenderer from 'react-test-renderer'; // ES5 with npm const TestRenderer = require('react-test-renderer');
const comp = TestRenderer.create(<Footer content="Something" />); // { type: "footer", props: { content: "Something" }, children : ...} comp.toJSON(); comp.root.type; comp.root.props; comp.root.findAllByType("div"); comp.root.findByProps({"data-testid": "my-test-id"}); comp.root.findAll((i) => i.children.length > 0);
Check Test Renderer API for more functions.
Another example
import TestRenderer from 'react-test-renderer'; function MyComponent() { return ( <div> <SubComponent foo="bar" /> <p className="my">Hello</p> </div> ) } function SubComponent() { return ( <p className="sub">Sub</p> ); } const testRenderer = TestRenderer.create(<MyComponent />); const testInstance = testRenderer.root; expect(testInstance.findByType(SubComponent).props.foo).toBe('bar'); expect(testInstance.findByProps({className: "sub"}).children).toEqual(['Sub']);
Testing events in Test Renderer
It is possible to test events with TestRenderer but it is recommended that you use the React Test Library instead. But here we show it anyway:
const root = TestRenderer.create(<Counter />).root; const p = root.findByType("p"); TestRenderer.act(() => { p.props.onClick(); }); expect(p.props.children).toEqual([1]);
Debugging
Besides the “usual” debugging in your IDE, you can use screen.debug()
to output additional info.
// debug document screen.debug() // debug single element screen.debug(screen.getByText('test')) // debug multiple elements screen.debug(screen.getAllByText('multi-test'))