Testing React Components

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 and react-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 Cypresspuppeteer 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:

  1. render(<MyComponent />)
  2. 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'.
  3. You can fire events on the element, like a user would, for example fireEvent.click(myButton)
  4. You wait for page updates and use either the container that the render function returns or – preferably – the screen type.
  5. 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:

LabelTextfind by label or aria-label text content
PlaceholderTextfind by input placeholder value
Textfind by element text content
DisplayValuefind by form element current value
AltTextfind by img alt attribute
Titlefind by title attribute or svg title tag
Rolefind by aria role
TestIdfind by data-testid attribute

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 Match1+ MatchAwait?
getBythrowreturnthrowNo
findBythrowreturnthrowYes
queryBynullreturnthrowNo
getAllBythrowarrayarrayNo
findAllBythrowarrayarrayYes
queryAllBy[]arrayarrayNo

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.*/);
    });
});

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]);

About Author

Mathias Bothe Contact me

I am Mathias, born 38 years ago in Heidelberg, Germany. Today I am living in Munich and Stockholm. I am a passionate IT freelancer with more than 14 years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I am founder of bosy.com, creator of the security service platform BosyProtect© and initiator of several other software projects.