A Place for Jest Snapshot Testing
Exploring several use cases for Jest Snapshot Testing.

Motivation
First, I was late to testing on the front-end. Then, my testing strategy, as documented in A Skeptics Guide to Frontend Testing: Part 1, did not include any Jest snapshot testing.
More recently, while working on another series (An Opinionated Web Application Solution: Part 1), colleagues challenged me why I didn’t use them. That got me thinking about where they would be useful.
What Is It?
Snapshot tests are a very useful tool whenever you want to make sure your UI does not change unexpectedly.
A typical snapshot test case for a mobile app renders a UI component, takes a screenshot, then compares it to a reference image stored alongside the test. The test will fail if the two images do not match: either the change is unexpected, or the screenshot needs to be updated to the new version of the UI component.
— Jest — Snapshot Testing
Assuming you are already using Jest and Enzyme, incorporating snapshot testing is fairly simple with the enzyme-to-json library. As part of the Opinionated…article’s sample application, I illustrate its use of Jest Snapshot testing; specifically (the tested component, a child component, the test, and the snapshot).
note: The component names will make more sense later.
src/components/Page/PageView/Risky/Risky.tsx
import Fragile from 'COMPONENTS/Fragile';
import React from 'react';export default () => {
return (
<div>
<h2>Risky</h2>
<Fragile />
</div>
);
};
src/components/Fragile/Fragile.tsx
import React from 'react';
import styleLess from './style.less';export default () => {
return (
<div>
<div id={styleLess.root} style={{ backgroundColor: 'black' }}>
Fragile1
</div>
<div className="my_global">Fragile2</div>
</div>
);
};
src/components/Page/PageView/Risky/Risky.test.tsx
import Enzyme, { render, shallow } from 'enzyme';
import enzymeAdapterReact16 from 'enzyme-adapter-react-16';
import React from 'react';
import Risky from './Risky';Enzyme.configure({ adapter: new enzymeAdapterReact16() });const getDefaultProps = () => ({});describe('Risky component', () => {
it('shallow renders without crashing', () => {
const {} = getDefaultProps();
shallow(<Risky />);
}); it('render snapshot', () => {
const {} = getDefaultProps();
const wrapper = render(<Risky />);
expect(wrapper).toMatchSnapshot();
});
});
The resulting snapshot:
src/components/Page/PageView/Risky/__snapshots__/Risky.test.tsx.snap
// Jest Snapshot v1, https://goo.gl/fbAQLPexports[`Risky component render snapshot 1`] = `
<div>
<h2>
Risky
</h2>
<div>
<div
id="root"
style="background-color:black"
>
Fragile1
</div>
<div
class="my_global"
>
Fragile2
</div>
</div>
</div>
`;
Do Not Blindly Use
When I first wrote this article, I claimed that snapshot testing is not suitable for use in unit testing. This was based on large-part to reading up on other’s opinions on the topic; the folks who were against using snapshot testing were more articulate and passionate than the folks who were for it. What I learned, however, is not that I should not use snapshot testing, but rather I needed to be very intentional when doing so.
Good tests encode the developer’s intention, they don’t only lock in the test’s behavior without editorialization of what’s important and why. Snapshot tests lack (or at least, fail to encourage) expressing the author’s intent as to what the code does (much less why)
Kent C. Dodds (citing Tweet from Justin Searls) — Effective Snapshot Testing
It is difficult to mind and justify testing of declarative code. As pointed in these article, the task of testing declarative code ends up with a test that tells what the declaration itself does. But with less readability and intent. Indeed, declarative code is in fact formal specification.
Jean Carlo Emer — Front-end (React) Snapshot Testing with Jest: What is it for?
Writing tests first is a great guideline. I do it almost all the time. But I don’t do it all the time. It makes me angry when I change declarative code only to have a test that looks exactly the same fail as well.
OK, tests for declarations don’t really make me angry. But they do make me wonder if the developer who wrote the test really likes to answer the question, “are you sure that you’re sure that you want to do what you just said you want to do?”
An Anti-Pattern
First, I will suggest that you do not want to replace your smoke tests (does it render) with snapshot testing; however tempting to make this leap. For example the following component is about as simple as can be.
src/components/Page/PageView/Media/Media.tsx
import React from 'react';
import kittenJpg from './kitten.jpg';const Media = () => {
return (
<div>
<h2>Media</h2>
<img src={kittenJpg} />
</div>
);
};
export default Media;
The difference between a smoke test and a snapshot test is the highlighted text below.
src/components/Page/PageView/Media/Media.test.tsx (anti-pattern)
import Enzyme, { shallow } from 'enzyme';
import enzymeAdapterReact16 from 'enzyme-adapter-react-16';
import React from 'react';
import Media from './Media';Enzyme.configure({ adapter: new enzymeAdapterReact16() });const getDefaultProps = () => ({});describe('Media component', () => {
it('shallow renders without crashing', () => {
const {} = getDefaultProps();
const wrapper = shallow(<Media />);
expect(wrapper).toMatchSnapshot();
});
});
The resulting snapshot is:
src/components/Page/PageView/Media/__snapshots__/Media.test.tsx.snap (anti-pattern)
// Jest Snapshot v1, https://goo.gl/fbAQLPexports[`Media component shallow renders without crashing 1`] = `
<div>
<h2>
Media
</h2>
<img
src=""
/>
</div>
`;
The first observation is that test looks pretty much exactly like the JSX itself; your “I should not test declarative tests.” warning bells should be going off about now.
First, you need to think what the intention behind writing both the smoke and snapshot test; I would claim that it is the same. You want to assert that the component renders without crashing (what it renders it really not a testing concern).
So then, what is the harm of adding in the snapshot test (it feels pretty cool and magical)?
The problem is that every time you edit the component you will unintentionally break your snapshot test (your real intention is that it simply renders). This then leads to you to being habituated to blindly accepting new snapshots; because there would be so many false positives. At this point, you have defeated any benefit that snapshot testing provides and added an extra bit of work every time you edit a component.
Conditional Rendering
The following is an example of a good use of snapshot testing. The component is as follows:
src/components/Page/PageView/Async/AsyncView/AsyncView.tsx
import Todo from 'DUCKS/todos/Todo';
import { List } from 'immutable';
import React, { Component } from 'react';
import Todos from './Todos';interface AsyncViewProps {
error: boolean;
requested: boolean;
todos: List<Todo>;
fetchTodos(): void;
}export default class AsyncView extends Component<AsyncViewProps> {
public componentDidMount() {
const { fetchTodos } = this.props;
fetchTodos();
}
public render() {
const { error, requested, todos } = this.props;
if (requested) {
return <div>Requested</div>;
}
if (error) {
return <div>Error</div>;
}
if (todos.size === 0) {
return <div>No Todos</div>;
}
return (
<div>
<h2>Async</h2>
<Todos todos={todos.toJS()} />
</div>
);
}
}
In addition to the calling of fetchTodos on mounting there are four additional intentions expressed here; specifically it renders in different ways when:
- requested is true
- requested is false and error is true
- requested is false and error is false and todos.size is 0
- requested is false and error is false and todos.size is not 0
The key is that we want to be able to distinguish between the different rendering results; for example we want to catch if someone accidentally deletes the special handling of the case of requested is true.
So we can use snapshot testing to reflect the intention of “rendering in a particular way” as shown below:
src/components/Page/PageView/Async/AsyncView/AsyncView.test.tsx
import Todo from 'DUCKS/todos/Todo';
import Enzyme, { shallow } from 'enzyme';
import enzymeAdapterReact16 from 'enzyme-adapter-react-16';
import { List } from 'immutable';
import React from 'react';
import AsyncView from './AsyncView';Enzyme.configure({ adapter: new enzymeAdapterReact16() });const todoDefault = {
completed: false,
id: 0,
title: 'title',
userID: 0,
};
const sampleTodo = new Todo(todoDefault);
const sampleTodos = List([sampleTodo]);
const getDefaultProps = () => ({
error: false,
fetchTodos: jest.fn(),
requested: false,
todos: sampleTodos,
});describe('Async component', () => {
it('shallow renders without crashing', () => {
const { error, fetchTodos, requested, todos } = getDefaultProps();
shallow(
<AsyncView error={error} fetchTodos={fetchTodos} requested={requested} todos={todos} />
);
});
it('renders differently with requested', () => {
const { error, fetchTodos, todos } = getDefaultProps();
const wrapper = shallow(
<AsyncView error={error} fetchTodos={fetchTodos} requested={true} todos={todos} />
);
expect(wrapper).toMatchSnapshot();
}); it('renders differently with not requested and error', () => {
const { fetchTodos, requested, todos } = getDefaultProps();
const wrapper = shallow(
<AsyncView error={true} fetchTodos={fetchTodos} requested={requested} todos={todos} />
);
expect(wrapper).toMatchSnapshot();
}); it('renders differently with not requested not error and 0 todos', () => {
const { error, fetchTodos, requested } = getDefaultProps();
const todos = List<Todo>([]);
const wrapper = shallow(
<AsyncView error={error} fetchTodos={fetchTodos} requested={requested} todos={todos} />
);
expect(wrapper).toMatchSnapshot();
}); it('calls fetchTodos on mount', () => {
const { error, fetchTodos, requested, todos } = getDefaultProps();
shallow(
<AsyncView error={error} fetchTodos={fetchTodos} requested={requested} todos={todos} />
);
expect(fetchTodos.mock.calls.length).toBe(1);
});
});
with resulting snapshot:
src/components/Page/PageView/Async/AsyncView/__snapshots__/AsyncView.test.tsx.snap
// Jest Snapshot v1, https://goo.gl/fbAQLPexports[`Async component renders differently with not requested and error 1`] = `
<div>
Error
</div>
`;exports[`Async component renders differently with not requested not error and 0 todos 1`] = `
<div>
No Todos
</div>
`;exports[`Async component renders differently with requested 1`] = `
<div>
Requested
</div>
`;
Observations:
- It is important to note that we used a shallow render for the test; we are doing an unit test (so we are not testing the dependencies).
- I did not use a snapshot test for the “normal case”, i.e., the one that shows the list of todos; this was intentional as this is the case that we likely will edit the output in the future (want to avoid the false-positive failures).
- Instead of using snapshots, we could have use Enzyme to find the key phrases (Error, No Todos, and Requested) for the test; this however is time consuming and error-prone.
Integration Regression Testing
Another scenario that I have heard colleagues mention the value of Jest snapshot testing is to do integration (with other components) regression (identifying changes) testing. The specific example that comes to mind is to help identify unexpected and problematic changes in the application due to changes in a dependency (say a shared component).
In the first example above we had the following that illustrates this scenario:
- Risky.tsx: The parent component that has a high risk of problem due to a fragile child component
- Fragile.tsx: The fragile child component that is the source of the problem (the example is actually fine, just imagine that this was a fragile component).
- Risky.test.tsx: In addition to an unit test (the smoke test) it includes a snapshot test. Here we do a deep (render) instead of the shallow render; we are testing the integration.
Wrap Up
It turns out that snapshot testing is useful in a number of situations. At the same time, it must be used with care to avoid generating false positive failures with ultimately make these tests useless (as you will begin to ignore them).
✉️ Subscribe to CodeBurst’s once-weekly Email Blast, 🐦 Follow CodeBurst on Twitter, view 🗺️ The 2018 Web Developer Roadmap, and 🕸️ Learn Full Stack Web Development.