Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / DevOps / testing

Snapshot Testing React with Jest

5.00/5 (2 votes)
4 Apr 2017CPOL11 min read 7.5K  
Snapshot Testing React with Jest

Introduction

Testing is a double-edged sword. On the one hand, having a solid test suite makes code easier to refactor, and gives confidence that it works the way it should. On the other hand, tests must be written and maintained. They have a cost, like any other code.

In a magical world, we could write our code, and then verify that it works with very little extra code.

Snapshot tests come close to offering this dreamy future. In this tutorial, we will go over what snapshot tests are and how to start using them with React.

What is a Snapshot Test?

A snapshot test verifies that a piece of functionality works the same as it did when the snapshot was created. It's like taking a picture of an app in a certain state, and then being able to automatically verify that nothing has changed.

I used the word "picture" there, but the snapshot tests we'll be looking at have nothing to do with images or screenshots. They are purely textual.

Here's an example. Let's say you created a React component which renders a list of 3 things, like this:

A simple list of things

Once you have it working, you could manually take a "snapshot" of it by copying and pasting its HTML representation into a file.

<code class="language-html"><ul class="todo-list">
  <li class="todo-item">A New Hope</li>
  <li class="todo-item">The Empire Strikes Back</li>
  <li class="todo-item">Return of the Jedi</li>
</ul>

Then, later on, you could verify that the component still works correctly by rendering it with the same data, and comparing the rendered HTML against the saved snapshot.

This is, essentially, what a snapshot test does. The first time it is run, it saves a textual snapshot of the component. Next time it runs (and every time thereafter), it compares the rendered component to the snapshot. If they differ, the test fails. Then, you have the opportunity to either update the snapshot, or fix the component to make it match.

Write the Component First

An important consequence of the way snapshot tests work is that the component should already work before you write a test for it. Snapshot testing is not test-driven development.

Strict test-driven development follows the "red-green-refactor" pattern: write a failing test, then write enough code to make that test pass, then refactor if necessary.

Snapshot testing, in contrast, follows something like a "green-green-refactor" approach: make the component work, then write a test to take a snapshot, then refactor if necessary.

TDD purists may think this sounds bad. We recommend thinking of snapshot testing as a tool in your arsenal - just one tool. It's not a solution to every testing situation, just like TDD isn't perfectly suited to every situation.

Likewise, snapshot testing doesn't entirely replace other testing libraries and techniques. You can still use Enzyme and ReactTestUtils. You should still test Redux parts (actions, reducers, etc.) in isolation.

Snapshot testing is a new tool to add to your toolbelt. It's not a whole new toolbelt.

Try It Out

Now that we have the theory covered, let's see what these snapshot tests look like and write a few of them.

If you don't have an existing project, create one with Create React App and follow along:

  • Install node and npm if you don't already have them
  • Install Create React App by running this command:
npm install -g create-react-app
  • Create a project by running:
create-react-app snapshot-testing

Introducing Jest

The tool we'll be using to run these tests is called Jest. It is a test runner that also comes with expectations (the expect function) and mocks and spies. If you've done some testing before, you may be familiar with libraries like Mocha, Sinon, and Chai for handling these pieces - Jest provides everything in one package. The full API can be seen here. It also has the "snapshot testing" feature we'll be using here, which no other tools currently have.

If you have an existing project that you'd like to add snapshot testing to, I will point you to the official documentation rather than duplicate it here. Even if you plan to integrate Jest into your own project, we suggest using Create React App and following the rest of this tutorial to get a feel for how snapshot testing works. For the rest of this tutorial, we'll assume you're using Create React App.

The project that Create React App generates comes with one test to start with. Try it out and make sure everything is working by running this command in the terminal:

npm test

This one command will run all the tests in "watch" mode. This means that after running all the tests once, it will watch for changes to files, and re-run the tests for the files that change.

You should see something like this:

One passing test

Jest's built-in watch mode is one of the best things about it. Unlike most other testing tools that simply show you successes and failures, Jest goes out of its way to make testing easier. The team at Facebook has clearly been working at making the developer experience great.

It will only re-run tests in files that have changed - but it even goes one step further, and will re-run tests for files that import the files that changed. It knows about your project dependency tree and uses that to intelligently reduce the amount of work it needs to do.

Jest will also help you manage your snapshots by telling you when they are no longer used, and you can easily clean them up by pressing the "u" key.

At the bottom, you can see that there are a few commands you can issue. One of them is q, to quit. Hit q now, and we'll get ready to create our first snapshot test (you can also quit with Ctrl-C).

Setting Up Snapshot Testing

Let's take a look at the App.test.js file. It contains this single boilerplate test:

JavaScript
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

it('renders without crashing', () => {
  const div = document.createElement('div');
  ReactDOM.render(<App />, div);
});

This is not a snapshot test, but it does verify that the test runner (Jest) is working. So, let's add a real snapshot test.

First, we need to add one import at the top:

JavaScript
import renderer from 'react-test-renderer';

This is the Jest snapshot renderer, which we'll use in a second. It does not come preinstalled, however, so next we must install it. At the command line, run this:

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

Now, you can start the tests in watch mode again:

npm test

Did You Get an Error?

If you're using React 15.4, everything should work at this point. However, if you're using an older version of React, you might see this error:

Invariant Violation: ReactCompositeComponent: injectEnvironment() can only be called once.

You can read this Github issue for more information about why this fails, but if you are unable to use React 15.4 for some reason, add this line to the top of App.test.js, under the imports:

JavaScript
jest.mock('react-dom');

You should be able to run npm test again, and it should work.

Add a Snapshot Test

Now, for the first real snapshot test. Add this code at the bottom of App.test.js:

JavaScript
it('renders a snapshot', () => {
  const tree = renderer.create(<App/>).toJSON();
  expect(tree).toMatchSnapshot();
});

Let's go over what's happening here.

First, we're using an arrow function to create the test (the () => { part). If you're not familiar with them, don't worry: the () => { is equivalent to function() { in this case. It's just easier to write. Arrow functions also preserve the "this" binding, but we're not making use of that capability here.

Next, we call renderer.create and pass it a React element <App/> in JSX form. Contrast this with the ReactDOM.render in the test above. They both render the element, but renderer.create creates a special output that has a toJSON method.

This toJSON call is important: it turns the component representation into JSON, like it says, which makes it easier to save as a snapshot, and compare to existing snapshots.

You can see what it looks like if you add a console.log(tree) after the renderer.create line. Try removing the toJSON call too, and see what that object looks like.

Finally, the line expect(tree).toMatchSnapshot() does one of these two things:

  • If a snapshot already exists on disk, it compares the new snapshot in tree to the one on disk. If they match, the test passes. If they don't, the test fails.
  • If a snapshot does not already exist, it creates one, and the test passes.

By "already exists on disk", we mean that Jest will look in a specific directory, called __snapshots__, for a snapshot that matches the running test file. For example, it will look for App.test.js.snap when running snapshot comparisons in the App.test.js file.

These snapshot files should be checked into source control along with the rest of your code.

Here's what that snapshot file contains:

JavaScript
exports[`test renders a snapshot 1`] = `
<div
  className="App">
  <div
    className="App-header">
    <img
      alt="logo"
      className="App-logo"
      src="test-file-stub" />
    <h2>
      Welcome to React
    </h2>
  </div>
  <p
    className="App-intro">
    To get started, edit
    <code>
      src/App.js
    
     and save to reload.
  </p>
</div>
`;

You can see that it's basically just an HTML rendering of the component. Every snapshot comparison (a call expect(...).toEqualSnapshot()) will create a new entry in this snapshot file with a unique name.

Failed Snapshot Tests

Let's look at what happens when a test fails.

Open src/App.js and delete this line:

JavaScript
<h2>Welcome to React</h2>

Now run the tests, by running npm test. You should see output similar to this:

Output of a failed test

This is a diff, showing the differences between the expected output (the snapshot) and the actual output. Here's how to read it:

The lines colored green (with the - signs) were expected, but missing. Those are lines that the snapshot has, but the new test output does not.

The lines colored red (with the + signs) were not expected. Those lines were not in the snapshot, but they appeared in the rendered output.

Lines colored gray are correct, and unchanged.

To get a feel for how this works, put back the line you took out:

JavaScript
<h2>Welcome to React</h2>

When you save the file, the tests will automatically re-run, and should pass.

Try different combinations of small changes, and then look at the diff to see how it represents additions, deletions, and changes.

Certain kinds of changes, like trailing spaces, can be difficult to see in the diff output. If you look at the expected vs. actual output and can see no differences, spaces may be the culprit.

Updating Snapshot Tests

Now, let's say we wanted to make the header smaller. Change the h2 tags to h3. The test will fail.

Here's a great feature of Jest: all you need to do is hit the u key to replace the incorrect snapshots with the latest ones! Try it now. Hit u. The tests will re-run and pass this time.

Create a New Component with Tests

Now, let's create a new component and use snapshot tests to verify it works. It'll be a simple counter component that doesn't allow negative numbers.

Create a new file src/PositiveCounter.js, and paste in this code:

JavaScript
import React, { Component } from 'react';

export default class PositiveCounter extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  increment = () => {
    this.setState({
      count: this.state.count + 1
    });
  }

  decrement = () => {
    this.setState({
      count: Math.max(0, this.state.count - 1)
    });
  }

  render() {
    return (
      <span>
        Value: {this.state.count}
        <button className="decrement" 
        onClick={this.decrement}>&minus;</button>
        <button className="increment" 
        onClick={this.increment}>+</button>
      </span>
    );
  }
}

If we were writing normal unit tests, now would be a good time to write some. Or, if we were doing test-driven development, we might've already written a few tests. Those are still valid approaches that can be combined with snapshot testing, but snapshot tests serve a different purpose.

Before we write a snapshot test, we should manually verify that the component works as expected.

Open up src/App.js and import the new PositiveCounter component at the top:

import PositiveCounter from './PositiveCounter';

Then, put it inside the render method somewhere:

JavaScript
class App extends Component {
  render() {
    return (
      <div className="App">
      	 <PositiveCounter/>
      	 ...
      </div>
    );
  }
}

Start up the app by running npm start in the terminal, and you should see the new counter. If you still have the test watcher running, it will fail because the content of App has changed. Press u to update the test.

Try out the PositiveCounter component. You should be able to click "+" a few times, then "-" a few times, but the number should never go below 0.

Now that we know it works, let's write the snapshot tests.

Create a new file, src/PositiveCounter.test.js, and start it off like this:

JavaScript
import React from 'react';
import ReactDOM from 'react-dom';
import PositiveCounter from './PositiveCounter';
import renderer from 'react-test-renderer';

it('should render 0', () => {
  const tree = renderer.create(<PositiveCounter/>).toJSON();
  expect(tree).toMatchSnapshot();
});

If npm test isn't running, start it now. You should see "1 snapshot written in 1 test suite", and the test will pass. You can inspect the file src/__snapshots__/PositiveCounter.test.js.snap to see what it rendered.

Let's now add a test that increments the counter:

JavaScript
it('should render 2', () => {
  const component = renderer.create(<PositiveCounter/>);
  component.getInstance().increment();
  component.getInstance().increment();
  expect(component.toJSON()).toMatchSnapshot();
});

Jest will again report that it wrote 1 snapshot, and the test will pass. Inspecting the snapshot file will verify that it rendered a "2" for this test. Remember, though: we already verified that the component works correctly. All we're doing with this test is making sure it doesn't stop working, due to changes in child components, a refactoring, or some other change.

Here, we used the component.getInstance() function to get an instance of the PositiveCounter class, then called its increment method.

Notice that we're not actually "clicking" the button itself, but rather calling the method directly. At this time, Jest doesn't seem to have good facilities for finding child components. If we wanted to click the button itself, we could write this instead:

JavaScript
component.toJSON().children[3].props.onClick()

However, this is fairly brittle and difficult to write, especially if there are multiple levels of nesting. The only advantage to this is that it verifies the onClick function is bound correctly. If you need to do DOM interaction like this, it might be better to write that a separate test using Enzyme or ReactTestUtils.

Let's add one more test. This one will verify that the counter cannot go negative:

JavaScript
it('should not go negative', () => {
  const component = renderer.create(<PositiveCounter/>);
  component.getInstance().increment();
  component.getInstance().decrement();
  component.getInstance().decrement();
  expect(component.toJSON()).toMatchSnapshot();
});

Remember we've already tested this functionality manually- this is just cementing it in place. The test should pass.

Wrapping Up

In this article, we covered how to get set up with snapshot testing and write a few tests.

Snapshot tests are a quick and easy way to make sure your components continue to work through refactoring and other changes. It doesn't replace other styles of testing, such as using Enzyme or ReactTestUtils, but it augments them with a nice first-pass approach. With snapshot tests, you have even fewer excuses to write tests! Try them out in your own project.

Snapshot Testing React with Jest was originally published by Dave Ceddia at Dave Ceddia on April 04, 2017.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)