Snapshot testing React applications with Jest
Software Engineer
Automated software testing is especially important in large applications with lots of moving parts. It is smart to use several testing methods 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. Using snapshots helps you make sure 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.
In this tutorial, I will lead you through using Jest, a JavaScript testing framework, to create snapshots for testing a simple React.js web application. You will be using the snapshots you create with Jest to simulate changes in a React.js application. If constantly changing text assertions are painful, you may find that snapshot testing is a powerful antidote.
Prerequisites
To get the most from this tutorial, you’ll need the following:
- Node.js installed locally.
- A GitHub account.
- A CircleCI account.
- Basic knowledge of JavaScript, React.js, 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.
How a snapshot tests work
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.
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 test passes, when a snapshot test fails, and what actions take place.
Setting up the sample React app
In this tutorial, your application will consist of a simple React.js component with two buttons that increment and decrement the count when clicked.
Clone the repository, and checkout to the start-here
branch by running this command in your terminal:
git clone https://github.com/CIRCLECI-GWP/snapshot-testing-react-jest.git
cd snapshot-testing-react-jest
git checkout start-here
Next, you will need the following dependencies installed from the npm:
They are included in your package.json
file. To install them in your project, open your terminal (at the project’s root) and run:
npm install
Once the dependencies are installed, run the application:
npm start
This starts your application.
Your 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. Add this snippet to src/App.test.js
:
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(domTree).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, then saves it, and checks that it 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.
Run the test:
npm test
The command will output a response similar to this snippet:
PASS src/App.test.js
Jest Snapshot testing suite
✓ matches snapshot (5 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: 0.675 s
Ran all test suites related to changed files.
...
There is 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 the DOM tree of the component, including the parent selector elements and the child elements.
To better understand snapshots, open the elements
section in the tab where your React.js 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.
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, you 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, use the current test and make changes to your DOM tree. The change you will make is introducing a title <h1>COUNTER</h1>
to the component. This addition is done in the file Counter.js
as a single line:
...
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. (If Jest is still in watch mode, you won’t have to rerun).
Note: When Jest is in watch mode
, the application is being tracked, so any changes will trigger a rerun of the tests. In our case, it is already activated due to the npm test script in package.json
. However, if you’d be running Jest directly, specify it with the --watch
argument to activate watch mode
.
The test should fail.
Because these changes are expected, you will need to update your existing snapshot instead of changing the code to match the previous snapshot.
In the same terminal session, 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 them. After the snapshot update is triggered, your test is back to being happy and it passes beautifully.
PASS src/App.test.js
Jest Snapshot testing suite
✓ matches snapshot (5 ms)
› 1 snapshot updated.
Snapshot Summary
› 1 snapshot updated from 1 test suite.
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 1 updated, 1 total
Time: 0.105 s, estimated 1 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. Add this code snippet to the file App.test.js
:
...
import Counter from "./Counter";
describe("Jest Snapshot testing suite", () => {
...
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 your 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.
PASS src/App.test.js
Jest Snapshot testing suite
✓ matches snapshot (7 ms)
✓ Should render 3 after three increments (9 ms)
› 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 passed, 2 total
Time: 0.207 s, estimated 1 s
Ran all test suites related to changed files.
Integrating snapshot testing with CircleCI
Why run successful tests that no one else knows about? Instead, 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 the rest of 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:21.4.0
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 (in main
branch), making this an optional step.
However, if you followed the tutorial from the start-here
branch, and are using a different repository with the same CircleCI configuration, you will need to push the changes to the repository. Save this file, commit and push your changes to GitHub.
Go to the CircleCI dashboard and specifying your organization your repository is in (if you belong to several). Click Set up Project beside the repository name.
When prompted, select main
, which is your default branch. Then click Set Up Project. Your project will begin running on CircleCI.
There should be a green build on the CircleCI dashboard. Click it to review the build details.
Fantastic! Your builds are green and all your 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 verify changes. 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!