This tutorial covers:

  1. Setting up Jest snapshots
  2. Using snapshots to simulate UI changes for testing
  3. Using snapshot tests to determine whether changes were intentional

Automated tests are especially important in large applications that have lots of moving parts. It is smart to learn about many methods of testing applications so that you can provide as much coverage as possible. If you are not familiar with using snapshots in testing, read on. Snapshot tests are written as part of frontend test automation.

In this tutorial, I will lead you through using Jest, a JavaScript testing framework, to create snapshots for testing a simple React web application. Using Jest snapshots will help you ensure that your UI changes are deterministic and that you are aware when changes are made. Using that information, you can determine whether the changes were intended or not. We will be using the snapshots we create with Jest to simulate changes in a React application. If constantly changing text assertions are painful, you may find that snapshot testing is a powerful antidote.

Prerequisites

To follow along this tutorial, you’ll need the following:

  • NodeJS installed locally.
  • A GitHub account.
  • A CircleCI account.
  • Basic knowledge of JavaScript, React, Git and Jest.

Our tutorials are platform-agnostic, but use CircleCI as an example. If you don’t have a CircleCI account, sign up for a free one here.

Snapshot testing in Jest

Jest is a JavaScript testing framework that makes writing frontend tests like snapshots, unit tests, and component tests easy and efficient. Snapshot testing is a type of output comparison testing. This type of testing ensures that your application adheres to the quality characteristics and code values of your development team.

How a snapshot test works

Snapshot tests are useful when you want to make sure your UI does not change unexpectedly. A typical snapshot test case renders a UI component, takes a snapshot, then compares it to a reference snapshot file stored alongside the test. The test compares the current state of your application to the established snapshots and expected behavior.

Visualizing the snapshot testing process

The following image illustrates the process of snapshot testing in Jest. It shows the different outcomes when a snapshot passes, when it fails, and what actions take place.

Snapshot testing

Setting up the sample React app

In this tutorial, our application will consist of a simple React component with two buttons that increment and decrement the count when clicked.

Clone the repository by running this command in your terminal:

git clone https://github.com:mwaz/jest-snapshot-testing.git;

cd jest-snapshot-testing;

Next, you will need the following dependencies installed from the npm React testing library, Jest, and the React test renderer.

They are installed in our package.json file. To install them in your system, open your terminal and run:

npm install

Once the dependencies are installed, run the application using this command:

npm start

This starts your application.

Jest test application

Our test will determine whether the counter value initializes at zero, and also whether the buttons work. Because the value changes depending on the button that is clicked, you may want to know that the incremental or decremental value stays the same, always increasing or decreasing by one. You can now start writing your tests.

Writing snapshot tests

Jest uses regular expressions to look for a file with the .test.js or .test.jsx extensions. Once the test files with these extensions are encountered, Jest will automatically run tests in those files when the test command is executed.

To write your first snapshot test, you will use the renderer module. This module renders the Document Object Model (DOM) element that will be saved as the text snapshot:

import renderer from "react-test-renderer";

Write your test to ensure that it captures the render of the <App> component and saves it as a Jest snapshot. This is the structure for the test:

import React from "react";
import renderer from "react-test-renderer";
import App from "./App";

describe("Jest Snapshot testing suite", () => {
  it("Matches DOM Snapshot", () => {
    const domTree = renderer.create(<App />).toJSON();
    expect(tree).toMatchSnapshot();
  });
});

The test has the domTree variable, which holds the DOM tree of the rendered component in JSON format. This makes it easier to save and compare snapshots. expect(domTree).toMatchSnapshot() creates a snapshot if it does not exist, saves it, and checks if a snapshot is consistent with previous stored snapshots. If there is an existing snapshot, Jest compares the two snapshots. If they match, the test passes. Snapshots that do not match cause the test to fail. The test also uses the .toJSON() method, which returns a JSON object of the rendered DOM tree snapshot.

Once you run the test (using the command npm test), there will be a new folder called (__snapshots__) with the file App.test.js.snap inside it. The file contains the saved snapshot, which should be similar to this snippet:

exports[`Jest Snapshot testing suite matches snapshot 1`] = `
<div
  className="App"
>
  <div
    className="counter"
  >
    <div
      className="buttons"
    >
      <button
        onClick={[Function]}
      >
        Increment
      </button>
      <button
        onClick={[Function]}
      >
        Decrement
      </button>
    </div>
    <p>
      0
    </p>
  </div>
</div>
`;

This snapshot file shows how the DOM tree of the component looks, including the parent selector elements and the child elements.

To better understand snapshots, open the elements section in the tab where your React application is running. Compare it side by side with the snapshot; they should be almost identical. Snapshots have a structure similar to the DOM, which makes the process of identifying changes to the DOM seamless.

Application DOM structure

The fact that text snapshots are created from the DOM means they can fail only when the DOM has changed or has content that differs from what was present when the snapshot was taken. Next, we will investigate how changes happen in the DOM, how they trigger snapshot changes, and how to handle this process.

Handling snapshot changes

Now that you know how snapshots are created, it is time to learn more about when they fail and why. To demonstrate this, we will use the above test and make changes to our DOM tree. The change we will make is introducing a title <h1>COUNTER</h1> to the component. This addition is shown in the file Counter.js:

return (
      <div className="counter">
        <h1>COUNTER</h1>
        <div className="buttons">
          <button onClick={this.increment}>Increment</button>
          <button onClick={this.decrement}>Decrement</button>
        </div>
        <p>{this.state.count}</p>
      </div>
    );

After making this change, run the test again. The test should fail.

Failed snapshot

Because these changes are expected, you will need to update your existing snapshot instead of changing the code to match the previous snapshot. Select the u option to update the snapshot when Jest is in watch mode. Updating the snapshot tells Jest that the changes were intentional and that you want to keep. After the snapshot update is triggered, your test is back to being happy and it passes beautifully.

Note: When Jest is in watch mode, the application is being tracked, so any changes will trigger a rerun of the tests. To activate watch mode, specify it in Jest runs with the --watch argument.

 PASS  src/App.test.js (20.905 s)
  Jest Snapshot testing suite
    √ Matches Snapshot (64 ms)

 > 1 snapshot updated.
Snapshot Summary
 > 1 snapshot updated from 1 test suite.
   ↳ src/App.test.js

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 updated, 1 total
Time:        51.348 s
Ran all test suites related to changed files.

Adding more snapshot tests

You can add more snapshot tests to make sure that all important visual elements in your application are consistent with your UI specifications and UX guidelines and that everything in your application works correctly. Snapshot tests are part of comprehensive frontend testing that should also include unit and component tests.

For this tutorial, I will run through adding just one more snapshot test. This test checks that the increment functionality works as intended. This code snippet can be found in the file App.test.js:

it("Should render 3 after three increments", () => {
    const component = renderer.create(<Counter />);
    component.getInstance().increment();
    component.getInstance().increment();
    component.getInstance().increment();
    expect(component.toJSON()).toMatchSnapshot();
});

In this test, the counter component structure is saved to a component variable. The test then accesses the increment() method of our class-based component and calls it three times. The goal is to make sure that when the Increment button is clicked three times, the count rendered is three. This information is saved to the snapshot, which should pass.

Integrating snapshot testing with CircleCI

A wise man (myself) once asked: “Why run successful tests that no one else knows about?” Take a moment to share your tests with the rest of the team, so that they can also benefit from the insights they provide.

For this tutorial, I will lead you through the steps for using CircleCI to execute your snapshot tests.

In the root folder of your application, create a .circleci folder, and add a config.yml file. The file will contain all the configuration required for running your CircleCI pipelines.

In the CircleCI config.yml file, add this configuration:

version: 2.1
jobs:
  build:
    working_directory: ~/repo
    docker:
      - image: cimg/node:14.17.1
    steps:
      - checkout
      - restore_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
      - run:
          name: install dependencies
          command: npm install
      - save_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
          paths:
            - ./node_modules
      - run:
          name: Jest snapshot tests
          command: npm test
      - store_artifacts:
          path: ~/repo/jest-snapshot-testing

Pushing your changes to GitHub

If you cloned the repository the changes already exist, making this an option step. However, if you are using a different repository and using the same configuration, you will need to push the changes to the repository.

Save this file, commit and push your changes to your GitHub repository. After navigating to the CircleCI dashboard, click Set up Project beside the repository name.

When prompted select main, which is our default branch. Then click Set Up Project. Your project will begin running on CircleCI.

CircleCI branch configuration

There should be a green build on the CircleCI dashboard! Click to review the build details.

Successful build

Fantastic! Our builds are green and all our tests executed successfully.

Conclusion

In this tutorial, you have learned about snapshot testing and how useful it is in making sure your UI looks and works as intended. You learned how to write a snapshot test, and used a snapshot as a comparison to ensure that any changes made were intended. You also learned how to update snapshots in case there are intentional changes. Finally you integrated CircleCI to run your tests. I hope you have enjoyed working on the project for this tutorial. Until next time, keep on coding!


Waweru Mwaura is a software engineer and a life-long learner who specializes in quality engineering. He is an author at Packt and enjoys reading about engineering, finance, and technology. You can read more about him on his web profile.