Introduction to the React Testing Library

Jan 29, 2019, updated Feb 13, 2019

React Testing Library is an amazing yet simple testing library from Kent Dodds. It works alongside the testing library Jest to provide React specific testing for snapshots, verifying DOM attributes or content, triggering click (or other) events, etc... you would use it in place of Enzyme, which although very useful, can tend to be quite a bit more complicated given that it has 3 different ways to render/mount your React components.

In this article we'll be covering the basics of React Testing Library, starting with the bare minimum and working our way up through the examples found in the docs.

The code for this article can be found on GitHub at https://github.com/leighhalliday/learning-react-testing-library

Snapshots

Snapshot tests allow you to take a snapshot (hence the name) of the HTML produced from your React component. By having this, you'll be made aware when you change your component in some way that does not produce the same output as you previously expected. If the output does differ, you can then make a decision to update the snapshot, or to fix your code so that no difference is found.

The component we'll be taking a snapshot of takes in a text prop, and puts it into a header and h1 tag.

import React from "react";

default function Header({ text }) {
return (
<header>
<h1>{text}</h1>
</header>
);
}

After importing the necessary packages, we can write a test which calls the render function provided by React Testing Library, which returns an object that we can extract asFragment from. Using this we can use the built-in functionality from Jest to perform a snapshot test.

import React from "react";
import { render, cleanup } from "react-testing-library";
import Header from "./Header";

afterEach(cleanup);

it("renders", () => {
const { asFragment } = render(<Header text="Hello!" />);
expect(asFragment()).toMatchSnapshot();
});

DOM selectors and expectations

If you would like to isolate specific DOM elements to test their content or properties they might have, this is when we'll want to use one of the provided getter/selector functions provided by RTL. In this example we'll look at getByTestId and getByText. For this test let's modify our component slightly, adding a data attribute and a class which we'll be testing for.

export default function Header({ text }) {
return (
<header>
<h1 data-testid="h1tag" className="fancy-h1">
{text}
</h1>
</header>
);
}

For our test, let's ensure the h1 tag contains a specific class and has the text content we are expecting. For that we'll add a package called jest-dom that will add on some expectations to Jest. Make sure to add the additional import to your test file (as shown below).

import React from "react";
import { render, cleanup } from "react-testing-library";
import "jest-dom/extend-expect";
import Header from "./Header";

afterEach(cleanup);

it("inserts text in h1", () => {
const { getByTestId, getByText } = render(<Header text="Hello!" />);

expect(getByTestId("h1tag")).toHaveTextContent("Hello!");
expect(getByText("Hello!")).toHaveClass("fancy-h1");
});

This time we used the 2 getter functions to find our DOM element, and then used toHaveTextContent and toHaveClass to ensure our DOM element matches our expectations.

Firing events

With React Testing Library it's very easy to simulate browser events such as a click event. The library comes with a function called fireEvent which handles this. Let's first look at the small component we'll be working with:

import React, { useState } from "react";

default function Clickers() {
const [count, setCount] = useState(0);

const increase = () => {
setCount(count + 1);
};
const decrease = () => {
setTimeout(() => {
setCount(count - 1);
}, 250);
};

return (
<div>
<button onClick={increase}>Up</button>
<button onClick={decrease}>Down</button>
<span data-testid="count">{count}</span>
</div>
);
}

We are showing 2 buttons to increment or decrement a count that were storing in state via setState. The increase happens immediately, but the decrease happens asynchronously with a 250ms delay.

We'll start with an initial test just to make sure it's rendering the state correctly... this will also include all of the imports we need for subsequent tests even though we aren't using them just yet.

import React from "react";
import {
render,
cleanup,
fireEvent,
waitForElement
} from "react-testing-library";
import "jest-dom/extend-expect";
import Clickers from "./Clickers";

afterEach(cleanup);

it("displays the count", () => {
const { getByTestId } = render(<Clickers />);
expect(getByTestId("count")).toHaveTextContent("0");
});

We used the getByTestId function to find the element and then were able to use the toHaveTextContent expectation function to ensure it has the value we are expecting. Now let's see how we can click the button which says "Up".

it("increments count", () => {
const { getByTestId, getByText } = render(<Clickers />);
fireEvent.click(getByText("Up"));
expect(getByTestId("count")).toHaveTextContent("1");
});

This test isn't too different from the first one, but we used fireEvent.click, passing it the element we wanted to click. This simulates how a user would interact with the button, and we can then ensure that the count element was updated to contain the value "1".

Async code... waiting for an element

In the example component about the decrease function happens asynchronously... it has a 250ms delay thanks to our friend setTimeout. So we can't do the same test we did about to test the increase function... we have to deal with the asynchronous nature of our code.

For this we will first make our Jest test function contain the async keyword, allowing us to use await inside of it to wait for a promise to resolve. We can then use waitForElement to wait patiently to find the element we're or change that we are looking for.

it("decrements count delayed", async () => {
const { getByText } = render(<Clickers />);
fireEvent.click(getByText("Down"));

const countSpan = await waitForElement(() => getByText("-1"));
expect(countSpan).toHaveTextContent("-1");
});

The waitForElement function takes an arrow function which should return the element: () => getByText("-1"). In this case we're just confirming that it does have "-1" as its text content, which is definitely redundant because we used "-1" to actually find the element, but it does the job.

Async code with Axios

The component we'll be testing here performs an AJAX call using the Axios library. Because we want to avoid real HTTP requests during testing we'll have to mock the Axios library for this test, and because of the async nature of this code we'll have to utilize the waitForElement function again to wait until expected element has been rendered by our component.

The Fetch component we are testing is below... I will include the useAxios function I created, but don't let it throw you off as I'll be covering this in depth in another article + video.

import React, { useState, useEffect } from "react";
import axios from "axios";

const useAxios = (url, setData) => {
useEffect(
() => {
let mounted = true;

const loadData = async () => {
const result = await axios.get(url);
if (mounted) {
setData(result.data);
}
};
loadData();

return () => {
mounted = false;
};
},
[url]
);
};

default function Fetch({ url }) {
const [data, setData] = useState(null);
useAxios(url, setData);

if (!data) {
return <span data-testid="loading">Loading data...</span>;
}

return <span data-testid="resolved">{data.greeting}</span>;
}

If you focus on the Fetch component, you'll see that I added 2 data-testid props so that I can ensure it correctly displays loading data on the first render, and then displays the real data once the useAxios function has called the setData function to update our state, forcing a re-render of the component.

If the file above lived in Fetch.js, our test will live in Fetch.test.js, and start with the usual imports needed to use React with react-testing-library.

import React from "react";
import { render, cleanup, waitForElement } from "react-testing-library";
import "jest-dom/extend-expect";
import axiosMock from "axios";
import Fetch from "./Fetch";

afterEach(cleanup);

Because we have added axios.js to the __mocks__ folder, the axios import is actually importing our mocked version rather than the real one, and it looks like:

export default {
get: jest.fn().mockResolvedValue({ data: {} })
};

I'll annotate the test itself by adding comments, explaining why each line is there and what it does.

it("fetches and displays data", async () => {
// We'll be explicit about what data Axios is to return when `get` is called.
axiosMock.get.mockResolvedValueOnce({ data: { greeting: "hello there" } });

// Let's render our Fetch component, passing it the url prop and destructuring
// the `getByTestId` function so we can find individual elements.
const url = "/greeting";
const { getByTestId } = render(<Fetch url={url} />);

// On first render, we expect the "loading" span to be displayed
expect(getByTestId("loading")).toHaveTextContent("Loading data...");

// Because the useAxios call (useEffect) happens after initial render
// We need to handle the async nature of an AJAX call by waiting for the
// element to be rendered.
const resolvedSpan = await waitForElement(() => getByTestId("resolved"));

// Now with the resolvedSpan in hand, we can ensure it has the correct content
expect(resolvedSpan).toHaveTextContent("hello there");
// Let's also make sure our Axios mock was called the way we expect
expect(axiosMock.get).toHaveBeenCalledTimes(1);
expect(axiosMock.get).toHaveBeenCalledWith(url);
});

Working with Redux

Coming soon... workin' on it.

Working with Reach Router

Coming soon... workin' on it.

Working with React Router

Coming soon... workin' on it.

Conclusion

The simplicity of React Testing Library makes it a joy to work with, only exposing enough functionality to write tests which mimic the real user experience with a focus on accessibility. Give this library a try the next time you're tempted to reach for Enzyme.