Testing React

Part 1, Chapter 5


Let's look at testing React components.


Installing

Create React App uses Jest, a JavaScript test runner, by default, so we can start writing test specs without having to install a runner. Along with Jest, we'll use React Testing Library, a fantastic yet surprisingly simple testing library made specifically for testing React components.

Install it:

$ npm install --save-dev @testing-library/[email protected]

For more on setting up React Testing Library, review the official docs.

With that, run the tests:

$ npm test

You should see:

No tests found related to files changed since last commit.

By default, the tests run in watch mode, so the tests will re-run every time you save a file.

Testing Components

Add a new directory called "__tests__" within the "components" directory. Then, create a new file called UsersList.test.jsx in "__tests__":

import React from 'react';
import { render, cleanup } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';

import UsersList from '../UsersList';

afterEach(cleanup);

const users = [
  {
    'active': true,
    'email': '[email protected]',
    'id': 1,
    'username': 'michael'
  },
  {
    'active': true,
    'email': '[email protected]',
    'id': 2,
    'username': 'michaelherman'
  }
];

it('renders a username', () => {
  const { getByText } = render(<UsersList users={users}/>);
  expect(getByText('michael')).toHaveClass('username');
  expect(getByText('michaelherman')).toHaveClass('username');
});

In this test, we used React Testing Library's render method to mount the UsersList component along with the appropriate props. render exposes a number of helpful utility methods. We used getByText to ensure that the p tag has the expected content. Then, with the help of jest-dom, which provides custom DOM element matchers for Jest, we ensured the p tag contains the username class. Finally, the cleanup method unmounts the component after the test is complete.

Install jest-dom:

$ npm install --save-dev @testing-library/[email protected]

Run the test to ensure it passes.

 PASS  src/components/__tests__/UsersList.test.jsx
  ✓ UsersList renders a username (6ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.243s, estimated 1s
Ran all test suites.

When writing tests, it's a good idea to run each test in isolation to ensure that each can be run mostly on their own. This makes it easier to run tests in parallel and it helps reduce flaky tests. When there's shared testing code, it's easy for other developers to change that code and have other tests break or produce false positives. Don't worry so much about keeping your tests DRY, in other words.

Snapshot Testing

Snapshot tests allow you to take a "snapshot" of the DOM rendered by your components. During each test run, the current snapshot is compared to the previous one. The test will fail if they do not match. They are great for testing less defined behavior and work best with less complex components that are not updated very often.

Add the following test case:

it("renders", () => {
  const { asFragment } = render(<UsersList users={users}/>);
  expect(asFragment()).toMatchSnapshot();
});

Run the tests:

 PASS  src/components/__tests__/UsersList.test.jsx
  ✓ renders a username (8ms)
  ✓ renders (13ms)1 snapshot written.
Snapshot Summary
 › 1 snapshot written from 1 test suite.

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   1 written, 1 total
Time:        0.367s, estimated 1s
Ran all test suites.

After the first test run, a snapshot of the component output was saved to the "__snapshots__" folder:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<DocumentFragment>
  <div>
    <p
      class="box title is-4 username"
    >
      michael
    </p>
    <p
      class="box title is-4 username"
    >
      michaelherman
    </p>
  </div>
</DocumentFragment>
`;

During subsequent test runs the new output will be compared to the saved output. Again, the test will fail if they differ.

Let's run a quick sanity check!

With the tests in watch mode, change {user.username} to {user.email} in the UsersList component. Save the change to trigger a new test run. You should see both tests failing, which is exactly what we want:

 FAIL  src/components/__tests__/UsersList.test.jsx
  ✕ renders a username (10ms)
  ✕ renders (6ms)

  ● renders a username

    Unable to find an element with the text: michael. 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.

    <body>
      <div>
        <div>
          <p
            class="box title is-4 username"
          >
            [email protected]
          </p>
          <p
            class="box title is-4 username"
          >
            [email protected]
          </p>
        </div>
      </div>
    </body>

      24 | it('renders a username', () => {
      25 |   const { getByText } = render(<UsersList users={users}/>);
    > 26 |   expect(getByText('michael')).toHaveClass('username');
         |          ^
      27 |   expect(getByText('michaelherman')).toHaveClass('username');
      28 | });
      29 |

      at getElementError (node_modules/@testing-library/dom/dist/query-helpers.js:46:10)
      at node_modules/@testing-library/dom/dist/query-helpers.js:100:13
      at node_modules/@testing-library/dom/dist/query-helpers.js:83:17
      at Object.getByText (src/components/__tests__/UsersList.test.jsx:26:10)

  ● renders

    expect(received).toMatchSnapshot()

    Snapshot name: `renders 1`

    - Snapshot
    + Received

      <DocumentFragment>
        <div>
          <p
            class="box title is-4 username"
          >
    -       michael
    +       [email protected]
          </p>
          <p
            class="box title is-4 username"
          >
    -       michaelherman
    +       [email protected]
          </p>
        </div>
      </DocumentFragment>

      30 | it("renders", () => {
      31 |   const { asFragment } = render(<UsersList users={users}/>);
    > 32 |   expect(asFragment()).toMatchSnapshot();
         |                        ^
      33 | });
      34 |

      at Object.toMatchSnapshot (src/components/__tests__/UsersList.test.jsx:32:24)1 snapshot failed.
Snapshot Summary
 › 1 snapshot failed from 1 test suite. Inspect your code changes or press `u` to update them.

Test Suites: 1 failed, 1 total
Tests:       2 failed, 2 total
Snapshots:   1 failed, 1 total
Time:        0.351s, estimated 1s
Ran all test suites.

Now, if this change was intentional, you can update the snapshot. To do so, you just need to press the u key:

Watch Usage
 › Press a to run all tests.
 › Press f to run only failed tests.
 › Press u to update failing snapshots.
 › Press i to update failing snapshots interactively.
 › Press q to quit watch mode.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press Enter to trigger a test run

Try it out. Press u. The tests will run again and the snapshot test should pass:

 FAIL  src/components/__tests__/UsersList.test.jsx
  ✕ renders a username (41ms)
  ✓ renders (9ms)

  ● renders a username

    Unable to find an element with the text: michael. 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.

    <body>
      <div>
        <div>
          <p
            class="box title is-4 username"
          >
            [email protected]
          </p>
          <p
            class="box title is-4 username"
          >
            [email protected]
          </p>
        </div>
      </div>
    </body>

      24 | it('renders a username', () => {
      25 |   const { getByText } = render(<UsersList users={users}/>);
    > 26 |   expect(getByText('michael')).toHaveClass('username');
         |          ^
      27 |   expect(getByText('michaelherman')).toHaveClass('username');
      28 | });
      29 |

      at getElementError (node_modules/@testing-library/dom/dist/query-helpers.js:46:10)
      at node_modules/@testing-library/dom/dist/query-helpers.js:100:13
      at node_modules/@testing-library/dom/dist/query-helpers.js:83:17
      at Object.getByText (src/components/__tests__/UsersList.test.jsx:26:10)1 snapshot updated.
Snapshot Summary
 › 1 snapshot updated from 1 test suite.

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   1 updated, 1 total
Time:        0.351s, estimated 1s
Ran all test suites.

Watch Usage: Press w to show more.

Once done, revert the changes we just made in the component and update the tests. Make sure they pass before moving on.

Test Coverage

Curious about test coverage?

$ ./node_modules/.bin/react-scripts test --coverage

You should see something like:

 PASS  src/components/__tests__/UsersList.test.jsx
  ✓ renders a username (30ms)
  ✓ renders (9ms)

----------------|----------|----------|----------|----------|-------------------|
File            |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
----------------|----------|----------|----------|----------|-------------------|
All files       |    27.27 |      100 |       25 |    27.27 |                   |
 src            |        0 |      100 |        0 |        0 |                   |
  index.js      |        0 |      100 |        0 |        0 |... 20,21,22,25,42 |
 src/components |      100 |      100 |      100 |      100 |                   |
  UsersList.jsx |      100 |      100 |      100 |      100 |                   |
----------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   1 passed, 1 total
Time:        2.396s
Ran all test suites related to changed files.

Testing Interactions

React Testing Library can also be used to test user interactions. We can fire events and then test that the actual results are the same as the expected results. We'll look at this in a future chapter.




Mark as Completed