Testing React

Part 2, Chapter 6


Let's look at testing React components.


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 Enzyme, a fantastic utility library made specifically for testing React components.

Install it as well enzyme-adapter-react-16:

$ npm install --save-dev [email protected] [email protected]

To configure Enzyme to use the React 16 adapter, add a new file to "src" called setupTests.js:

import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });

For more on setting up Enzyme, 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 { shallow } from 'enzyme';

import UsersList from '../UsersList';

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

test('UsersList renders properly', () => {
  const wrapper = shallow(<UsersList users={users}/>);
  const element = wrapper.find('h4');
  expect(element.length).toBe(2);
  expect(element.get(0).props.children).toBe('michael');
});

In this test, we used the shallow helper method to create the UsersList component and then we retrieved the output and made assertions against it. It's important to note that with "shallow rendering", we can test the component in complete isolation, which helps to ensure child components do not indirectly affect assertions.

For more on shallow rendering, along with the other methods of rendering components for testing, mount and render, see this Stack Overflow article.

Run the test to ensure it passes.

PASS  src/components/__tests__/UsersList.test.jsx
 ✓ UsersList renders properly (4ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.118s, estimated 1s
Ran all test suites related to changed files

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

Next, add a Snapshot test to ensure the UI does not change:

test('UsersList renders a snapshot properly', () => {
  const tree = renderer.create(<UsersList users={users}/>).toJSON();
  expect(tree).toMatchSnapshot();
});

Add the import to the top:

import renderer from 'react-test-renderer';

Run the tests:

PASS  src/components/__tests__/UsersList.test.jsx
 ✓ UsersList renders properly (3ms)
 ✓ UsersList renders a snapshot properly (9ms)

Snapshot Summary
› 1 snapshot written in 1 test suite.

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   1 added, 1 total
Time:        0.468s, estimated 2s
Ran all test suites related to changed files.

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[`UsersList renders a snapshot properly 1`] = `
<div>
  <h4
    className="box title is-4"
  >
    michael
  </h4>
  <h4
    className="box title is-4"
  >
    michaelherman
  </h4>
</div>
`;

During subsequent test runs the new output will be compared to the saved output. 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
  ✕ UsersList renders properly (6ms)
  ✕ UsersList renders a snapshot properly (2ms)

  ● UsersList renders properly

    expect(received).toBe(expected) // Object.is equality

    Expected: "michael"
    Received: "[email protected]"

      24 |   const element = wrapper.find('h4');
      25 |   expect(element.length).toBe(2);
    > 26 |   expect(element.get(0).props.children).toBe('michael');
         |                                         ^
      27 | });
      28 |
      29 | test('UsersList renders a snapshot properly', () => {

      at Object.toBe (src/components/__tests__/UsersList.test.jsx:26:41)

  ● UsersList renders a snapshot properly

    expect(value).toMatchSnapshot()

    Received value does not match stored snapshot "UsersList renders a snapshot properly 1".

    - Snapshot
    + Received

      <div>
        <h4
          className="box title is-4"
        >
    -     michael
    +     [email protected]
        </h4>
        <h4
          className="box title is-4"
        >
    -     michaelherman
    +     [email protected]
        </h4>
      </div>

      29 | test('UsersList renders a snapshot properly', () => {
      30 |   const tree = renderer.create(<UsersList users={users}/>).toJSON();
    > 31 |   expect(tree).toMatchSnapshot();
         |                ^
      32 | });
      33 |

      at Object.toMatchSnapshot (src/components/__tests__/UsersList.test.jsx:31:16)1 snapshot failed.
Snapshot Summary
 › 1 snapshot failed from 1 test suite. Inspect your code changes or re-run jest with `-u` to update them.

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

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

Watch Usage
 › Press a to run all tests.
 › Press f to run only failed tests.
 › Press p to filter by a filename regex pattern.
 › Press q to quit watch mode.
 › Press t to filter by a test name regex pattern.
 › Press u to update failing snapshots.
 › 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
  ✕ UsersList renders properly (7ms)
  ✓ UsersList renders a snapshot properly (2ms)

  ● UsersList renders properly

    expect(received).toBe(expected) // Object.is equality

    Expected: "michael"
    Received: "[email protected]"

      24 |   const element = wrapper.find('h4');
      25 |   expect(element.length).toBe(2);
    > 26 |   expect(element.get(0).props.children).toBe('michael');
         |                                         ^
      27 | });
      28 |
      29 | test('UsersList renders a snapshot properly', () => {

      at Object.toBe (src/components/__tests__/UsersList.test.jsx:26:41)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.12s, estimated 1s
Ran all test suites.

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?

$ react-scripts test --coverage

You may need to globally install React Scripts: npm install [email protected] --global.

 PASS  src/components/__tests__/UsersList.test.jsx
  ✓ UsersList renders properly (11ms)
  ✓ UsersList renders a snapshot properly (9ms)

----------------|----------|----------|----------|----------|-------------------|
File            |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
----------------|----------|----------|----------|----------|-------------------|
All files       |       25 |      100 |       25 |       25 |                   |
 src            |     7.69 |      100 |        0 |     7.69 |                   |
  index.js      |        0 |      100 |        0 |        0 |... 19,20,21,24,41 |
  setupTests.js |      100 |      100 |      100 |      100 |                   |
 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.32s
Ran all test suites.

Testing Interactions

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

It's worth noting that we'll focus much of our React testing on unit testing the individual components. We'll let end-to-end tests handle testing user interaction as well as the interaction between the client and server.

requestAnimationFrame polyfill error

Do you get this error when your tests run?

console.error node_modules/fbjs/lib/warning.js:33
    Warning: React depends on requestAnimationFrame.
    Make sure that you load a polyfill in older browsers.
    http://fb.me/react-polyfills

If so, add a new folder to "services/client/src/components" called "__mocks__", and then add a file to that folder called react.js:

const react = require('react');
// Resolution for requestAnimationFrame not supported in jest error :
// https://github.com/facebook/react/issues/9102#issuecomment-283873039
global.window = global;
window.addEventListener = () => {};
window.requestAnimationFrame = () => {
  throw new Error('requestAnimationFrame is not supported in Node');
};

module.exports = react;

Review this comment on GitHub for more info.




Mark as Completed