This is a sign
to start testing

Keep calm and test (and then test again and test one more time)

In this tutorial, I'll lead you through setting up and using Jest for snapshot testing a simple React web application. If you need a sign to start testing, this is it.

Table of contents

What is Jest?

There are many different ways of testing, in particular React applications. For instance, you can use React Testing Library or Enzyme for these purposes. In this article we'll focus on tests using Jest in pair with Test Renderer package (react-test-renderer)

Jest is a popular JavaScript testing framework which works with projects using React, Angular, Vue, etc. Initially, Jest was created by Facebook specifically for testing React applications. It's one of the most popular framework for testing React components. Thus, Jest can be easily integrated with React, that's why my choice fell on it.

Prerequisites

Let's begin by installing the required libraries and setting up the project. You can create a sample project for testing in any convenient way (e.g. by using Create React App) or implement it in already existing project, there are no any restrictions or requirements.

First, add Jest and other reqiured packages as a dev dependencies to the project. If your project based on mentioned above create-react-app, you'll only need to add react-test-renderer because Jest is already included out of the box in create-react-app:

npm install --save-dev react-test-renderer

If you have an existing application, as in my case, you'll need to install a few packages to make everything work well together:

npm install --save-dev jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer

We install the @types/jest package to provide types for Jest globals without a need to import them. Without this package you have to import globals as in the example below in all test files:

Component.test.tsx
1...
2import { describe, expect, test } from '@jest/globals'
3...

After installing required packages, add a new npm script to your package.json to make tests runnable:

"scripts": {
  ...
  "test": "jest"
}

Here's my package.json:

...
"dependencies": {
  "react": ...,
  "react-dom": ...,
  "react-scripts": ...
  ...
},
"devDependencies": {
  "@babel/preset-env": ...,
  "@babel/preset-react": ...,
  "@babel/preset-typescript": ...,
  "@jest/globals": ...,
  "@types/jest": ...,
  "babel-jest": ...,
  "jest": ...,
  "react-test-renderer": ...,
  "ts-jest": ...,
  "typescript": ...
},
...

Make sure you are using the same version of react and react-test-renderer. Otherwise you'll end up with a confusing TypeScript error such as cannot read properties of undefined (as it has happened with me).

Pay attention, Jest doesn't require any configuration to find test files. It goes through the whole project and looks for files that look like they're tests, for example files with .test.* extension. For your convenience, tests can be organized into special __tests__ folder under a top-level src directory. We'll get to that below.

Now we can run this command to fire up Jest and run all tests:

npm test

Let's run it to make sure nothing has broken yet because there're no test yet:

npm test

> test-test-test@0.1.0 test
> jest

No tests found, exiting with code 1
Run with `--passWithNoTests` to exit with code 0
GALLERYmeme.jpg
Roll Safe meme with text: tests cannot fail if there are no test

Introduction to Jest

Before testing we should take a look at some Jest basics. There are a few key concept which you need to know before diving in. Jest comes with three different built-in blocks for organizing tests. The most important are:

  • describe()
  • test()
  • it()

Method describe() creates a block that groups together several related tests. Whenever you start writing your test files from an empty file you may start with describe(). However, starting with describe() is not necessarily. The describe block may contain other different blocks that will be mentioned below.

Component.test.tsx
1describe('Component', () => {
2  ...
3});

You can nest describe() blocks into each other to further break down your tests and improve readability if needed. Meanwhile, nested blocks do not depend on each other to run.

Component.test.tsx
1describe('Component', () => {
2  describe("check some state", () => {
3  ...
4  });
5
6  describe("check another state", () => {
7    ...
8  });
9});

Method test(), as you can guess, runs a test, and method it()is just an alias of test(). Yep, it's confusing, but test() and it() are interchangeable according to the official API of Jest. You can use either of these.

Component.test.tsx
1describe('Component', () => {
2  test('test something', () => {
3    ...
4  });
5});

To run only one test with Jest, add only() after block name. This applies to test() as well as to its alias it(). In the example below the first test will be the only test that runs, and the second test will not run:

Component.test.tsx
1describe('Component', () => {
2  test.only('test something', () => {
3    ...
4  });
5
6  test('test another', () => {
7    ...
8  });
9});

Another way to focus on some tests is replacing it() with xit() to temporarily exclude a test from being executed. Similarly, fit() lets focus on a specific test without running any other tests.

Moreover, there are a few of useful methods in Jest which will be worth a lot in the future:

  • beforeEach()
  • beforeAll()
  • afterEach()
  • afterAll()
  • each()

Let's quickly go through all of them.

If you have some tests you need to do repeatedly for many times, you can use beforeEach() and afterEach(). beforeEach() runs before any other block and afterEach() runs after, respectively. For example, if you need to initialize something before testing and reset some things immediately after, you can do this with following:

Component.test.tsx
1describe('Component', () => {
2  beforeEach(() => {
3    initialize();
4  });
5
6  afterEach(() => {
7    reset();
8  });
9
10  test('test something', () => {
11    ...
12  });
13
14  test('test another', () => {
15    ...
16  });
17});

In the example above, initializing will be called before each of these two tests, and reset method will be called after each of them.

In some cases, the opposite situation is also possible when you only need to do something once at the beginning of a test file. Jest provides beforeAll() and afterAll() blocks to handle this. In the example below, initializing will be called before all of tests, and reset method will be called after all of them:

Component.test.tsx
1describe('Component', () => {
2  beforeAll(() => {
3    initialize();
4  });
5
6  afterAll(() => {
7    reset();
8  });
9
10  test('test something', () => {
11    ...
12  });
13
14  test('test another', () => {
15    ...
16  });
17});

There are some situation, when we need to write similar tests. For example, you may need to check all possible variants of a component property.

Button.test.tsx
1describe('Button', () => {
2  test('renders correctly', () => {
3    ...
4  });
5
6  test('renders disabled button', () => {
7    ...
8  });
9
10  test('renders focused button', () => {
11    ...
12  });
13
14  test('renders large button', () => {
15    ...
16  });
17});

The only difference between the two tests below are the variant of button. To avoid repetition, you can use each() that will alow to reduce all these tests into one. Using each() block, tests could be rewritten like this:

Button.test.tsx
1const variants = ...
2
3test.each(variants)(
4  'renders %s correctly',
5  (variant) => {
6    ...
7  }
8);

At last, but not least, writing tests is impossible without expect(). This method in most cases is used with its matchers. There are a lot of matchers: toBe(), toEqual(), toContain(), etc. You can find full list of Jest matchers in its documenation. Below we will consider a couple of them in more detail.

Let's not stray too far, in Jest documentation you can find more information about its API.

Snapshot testing

Finally, let's the tests! We'll create a snapshot test for a small component. Based on that you will be able to create tests for other components in a similar way.

Snapshot testing is popular use case. In the process of snapshot testing, as the name suggests, renders a UI component and then takes a snapshot to compare it to a reference snapshot file stored alongside the test. The test will fail if these two snapshots do not match to each other. What is the difference between snapshots and visual testing? Visual testing requires building the entire application. But instead you can use a test renderer to quickly generate snapshots for specific components. All generated snapshots you will find in your __tests__ directory in folder __snapshots__.

To create test just add code() or it() blocks with the name of the test and its code. You can optionally wrap them in describe() blocks for logical grouping but this not strict required.

Let's test a top-level React component <App />. This will be the easiest test to check correct rendering without crashing. To test we'll App.test.jsx file in the __tests__ folder and with the following code:

App.test.tsx
1import React from 'react';
2import renderer from 'react-test-renderer';
3
4import App from '../../App';
5
6describe('App', () => {
7  test('renders correctly', () => {
8    const tree = renderer
9      .create(<App />)
10      .toJSON();
11
12    expect(tree)
13      .toMatchSnapshot();
14  });
15});

If you are using JSX in your Jest test files, you'll need to add the following import line to the top of the file:

import React from 'react';

The reason for this is that Babel transforms the JSX syntax into a series of React.createElement() calls, and if you fail to import React, those will fail.

The test above mounts <App /> component and makes sure that it didn't throw during rendering. Let's find out what's going on the code above. Method create() create a Test Renderer instance with the passed React element, in this case <App /> component. Then toJSON() return an object representing the rendered tree. After that we should use mentioned above expect. It has a lot matchers, that let validate different things. One of this matchers is toMatchSnapshot that ensures that a value matches the most recent snapshot.

If you run test command after adding <App /> test you'll see the following output in your terminal:

npm test
> test-test-test@0.1.0 test
> jest

PASS  src/components/__tests__/App.test.tsx

App
  ✓ renders correctly (8 ms)

  › 1 snapshot written.

Snapshot Summary
  › 1 snapshot written from 1 test suite.

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 written, 1 total
Time:        2.656 s

Ran all test suites.

Also you will find a new file in __snapshots__ folder. This file will contain code below that is called snapshot:

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

exports[`App renders correctly 1`] = `
  <div>
    ...
  </div>
`;

If you run test again without changing test file, you'll get the following output:

npm test
> test-test-test@0.1.0 test
> jest

PASS  src/components/__tests__/App.test.tsx

App
  ✓ renders correctly (8 ms)

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

As you can see, there is not snapshot summary. That's because there are no changes in snapshots. Let's change something in <App /> component and run test command again. I've just changed component template and added a new <h1> element:

App.jsx
1...
2<div className='container'>
3  <h1>Yay!</h1>
4  ...
5</div>
6...

Now Jest will print this output:

npm test
> test-test-test@0.1.0 test
> jest

FAIL  src/components/__tests__/App.test.tsx
App
  ✕ renders correctly (10 ms)

● App › renders correctly

  expect(received).toMatchSnapshot()

  Snapshot name: `App renders correctly 1]`

    - Snapshot  - 0
    + Received  + 3

    @@ -1,8 +1,11 @@
      <div
        className="container"
      >
    +   <h1>
    +     Yay!
    +   </h1>
      11 |
      12 |     expect(tree)
    > 13 |       .toMatchSnapshot();
          |        ^
      14 |   });
      15 | });
      16 |

      at Object.<anonymous> (src/components/__tests__/App.test.tsx:13:8)

   › 1 snapshot failed.
  Snapshot Summary
   › 1 snapshot failed from 1 test suite. Inspect your code changes or run `npm test -- -u` to update them.

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

This snapshot test failed because the snapshot for updated component no longer matches a snapshot artifact for this test case. To resolve this, we will need to update the snapshot artifacts. To do this you can run Jest with special flag that will tell it to re-generate snapshots. Note extra -- in the middle of the command.

npm test -- -u

This command will re-generate snapshot artifacts for all failing snapshot tests. Let's complicate a task and create a new component <Button />:

Button.jsx
1import React, { ReactNode } from 'react';
2import classNames from 'classnames';
3
4export enum ButtonVariant {
5  INFO = 'is-info',
6  SUCCESS = 'is-success',
7  WARNING = 'is-warning',
8  DANGER = 'is-danger'
9}
10
11interface ButtonProps {
12  children: ReactNode;
13  variant?: ButtonVariant;
14  onClick?: () => void;
15}
16
17const Button: React.FC<ButtonProps> = ({ children, variant = ButtonVariant.INFO, onClick }: ButtonProps) => {
18  const bind = classNames.bind([]);
19
20  return (
21    <button
22      className={bind(['button', variant])}
23      onClick={onClick}>
24      {children}
25    </button>
26  );
27};
28
29export default Button;

You may have noticed, there's a property variant, that may have one of a certain value: INFO, SUCCESS, WARNING or ERROR. To test all of this button variants we can write the following test in a file Button.test.tsx:

Button.test.tsx
1import React from 'react';
2import renderer from 'react-test-renderer';
3
4import Button from '../Button';
5
6describe('Button', () => {
7  test('renders INFO correctly', () => {
8    const tree = renderer
9      .create(<Button
10        variant={ButtonVariant.INFO}>
11        Click me
12      </Button>)
13      .toJSON();
14
15    expect(tree)
16      .toMatchSnapshot();
17  });
18
19  test('renders SUCCESS correctly', () => {
20    const tree = renderer
21      .create(<Button
22        variant={ButtonVariant.SUCCESS}>
23        Click me
24      </Button>)
25      .toJSON();
26
27    expect(tree)
28      .toMatchSnapshot();
29  });
30
31  test('renders WARNING correctly', () => {
32    const tree = renderer
33      .create(<Button
34        variant={ButtonVariant.WARNING}>
35        Click me
36      </Button>)
37      .toJSON();
38
39    expect(tree)
40      .toMatchSnapshot();
41  });
42
43  test('renders ERROR correctly', () => {
44    const tree = renderer
45      .create(<Button
46        variant={ButtonVariant.WARNING}>
47        Click me
48      </Button>)
49      .toJSON();
50
51    expect(tree)
52      .toMatchSnapshot();
53  });
54});

Well, so far it doesn't look that bad, but if you add a new property, that may have one of an another certain value, you'll repeat yourself. There's a simple solution, that we've already mentioned above: each(). Take a look at how amazing test looks after refactoring:

Button.test.tsx
1import React from 'react';
2import renderer from 'react-test-renderer';
3
4import Button from '../Button';
5import { ButtonVariant } from './../Button/Button';
6
7const variants = Object.keys(ButtonVariant);
8
9describe('Button', () => {
10  test.each(variants)(
11    'renders %s correctly',
12    (variant) => {
13      const tree = renderer
14        .create(<Button
15          variant={variant as ButtonVariant}>
16          Click me
17        </Button>)
18        .toJSON();
19
20      expect(tree)
21        .toMatchSnapshot();
22    }
23  );
24});

It looks much better!

Note: I've used CSS framework Bulma in this example project. Used above is-info, is-success, etc are Bulma CSS classes for buttons.

Handling assets

If you're using any static files in your project, e.g. images, you'll probably face with an configuration error in the process of running tests:

SyntaxError: Invalid or unexpected token
  > 10 | import cover from './assets/cover.jpg';

Let's configure Jest to handle asset files. Usually, asset files aren't useful in tests so we can safely mock them out like this in jest.config.js:

/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
  ...
  moduleNameMapper: {
    '\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
      '<rootDir>/src/__mocks__/fileMock.js'
  }
  ...
};

Then create folder __mocks__ under a top-level src directory and add the following file in this folder:

module.exports = 'test-file-stub';

That's all! Now this assets problem must be solved.

Coverage reporting

Code coverage makes possible to spot untested paths in your code. Jest has an integrated coverage reporter that requires no configuration. Run the following command to include a coverage report like this. Again note extra -- in the middle of the command.

npm test -- --coverage

After running this command you'll see test coverage report in the output:

npm test -- --coverage
> test-test-test@0.1.0 test
> jest "--coverage"

PASS  src/components/__tests__/App.test.tsx
App
  ✓ renders correctly (13 ms)

  ------------------------|---------|----------|---------|---------|-------------------
  File                    | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
  ------------------------|---------|----------|---------|---------|-------------------
  All files               |     100 |      100 |     100 |     100 |
  src                     |     100 |      100 |      50 |     100 |
  App.tsx                 |     100 |      100 |      50 |     100 |
  ------------------------|---------|----------|---------|---------|-------------------

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

Recommended to run coverage test separately from your normal workflow because it runs much slower.

Conclusion

Testing doesn't hurt.

The full source code for this article can be found in this Github repository.

Read more

  1. Jest documentation
  2. Test Rendered documentation
  3. Jest snapshot testing documentation
  4. Configuring code coverage in Jest
  5. React Test Rendered documentation