If you are developing React applications, and looking to test your React components, Jest provides an easy-to-use tool with snapshot tests. In this article, we will see what snapshot testing is, write an example snapshot test in a React app, and discuss what pitfalls to avoid along with a few of the best use cases for utilizing this tool in order to do effective snapshot testing.

Snapshot testing

What is snapshot testing?

Snapshot tests allow to keep a check on unexpected changes in UI components. This is accomplished by taking a snapshot of a rendered component in its expected state and storing it as a reference to compare against future snapshots of the same component. So essentially, each time the snapshot test is run, the component is rendered and the new output is compared against the stored version. The test passes if the two snapshots match, and fails if the snapshots do not match.

If the test fails, in other words, the two snapshot versions differ, there can be two possibilities: the code is doing something unexpected or the newer snapshot is correct. We need to compare the difference to determine if this change is unexpected and the code needs to be fixed or if it is expected and the stored reference snapshot needs to be updated to the new version of the component.

This is a different approach from the conventional testing techniques that may be more commonly used. Regular assertion-based testing allow to write detailed and specific tests that capture developer intention. In contrast, snapshot testing can be used to easily test cases which cannot be clearly defined and are more susceptible to change, like UI components that may change in a varied number of ways based on different component states and user interactions.

Before delving further into the consequences of this distinction, we will first implement a simple snapshot testing example in the next section.

Getting started with snapshot testing

Let’s write a simple snapshot test in a Create React App example, to see how snapshot testing is done using Jest. First, we will generate a basic React app:

npx create-react-app snap-tasks

This will create the bootstrapped react project and place the associated files in a directory called snap-tasks in the current folder. From within the project directory (cd snap-tasks), we can run the application with the npm start command and then open http://localhost:3000/ to view the default app in the browser.

Create React App uses Jest as its test runner so it comes configured with Jest. We will only need to add react-test-renderer for rendering snapshots, which can be easily installed by running the command npm install --save-dev react-test-renderer. To run the tests in our project folder we can run the command npm test.

Next, we will add a new component called Tasks to this React app and then write snapshot tests for it. This first version of the Tasks component takes an array of task objects as a prop and either displays a message if the array is empty or displays each task’s detail in an unordered bullet list.

/* in Tasks.js */
import React from 'react'
import PropTypes from 'prop-types'

function Tasks(props) {
  const { tasks = [] } = props
  return (
    <ul>
      {tasks.length === 0 && (<li>No tasks to do yet!</li>)}
      {tasks.map((task, index) => 
 <li key={index}>{task.detail}</li>)}
    </ul>
  )
}

Tasks.propTypes = {
  tasks: PropTypes.array,
}

Tasks.defaultProps = {
  tasks: [],
}

export default Tasks

In order to render the Tasks component in our application view, we will update App.js. First, we will define an array of task objects, where each task object contains the task detail and a done boolean value to indicate the status of task completion. Then we will add the Tasks component with the array passed to it as a prop.

/* in App.js */
import React from 'react'
import './App.css'
import Tasks from './Tasks'

function App() {
  const tasks = [
    {detail: "Schedule meeting with TJ", done: false}, 
    {detail: "Reply to Rey's email", done: false}, 
    {detail:"Set up monthly coding meetup", done: false}
  ]
  return (
    <div>
      <header className="App-header">
        <h3> Tasks To-Do </h3>
        <Tasks tasks={tasks}/>
      </header>
    </div>
  )
}

export default App

Running and viewing this app in the browser should show us a bullet list with the task details.

Runing app

Finally, we are ready to add snapshot tests for this Tasks component. In the test suite for Tasks (in the file Tasks.test.js), we will write two tests to check the snapshot of the component when it renders without any tasks, and another snapshot when there are tasks.

/* in Tasks.test.js */
import React from 'react'
import renderer from 'react-test-renderer'
import Tasks from './Tasks'

it('renders correctly when there are no tasks', () => {
    const tree = renderer.create(<Tasks />).toJSON()
    expect(tree).toMatchSnapshot()
})

it('renders correctly when there are tasks', () => {
    const tasks = [
        {detail: "Schedule meeeting with TJ", done: false}, 
        {detail: "Reply to Rey's email", done: false}, 
        {detail:"Set up monthly coding meetup", done: false}
    ]
    const tree = renderer.create(<Tasks tasks={tasks} />).toJSON()
    expect(tree).toMatchSnapshot();
})

When we run this test suite for the first time, the tests will pass and Jest will generate the following two snapshots in __snapshots__/Tasks.test.js.snap.

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

exports[`renders correctly when there are no tasks 1`] = `
<ul>
  <li>
    No tasks to do yet!
  </li>
</ul>
`;

exports[`renders correctly when there are tasks 1`] = `
<ul>
  <li>
 Schedule meeeting with TJ
  </li>
  <li>
 Reply to Rey's email
  </li>
  <li>
 Set up monthly coding meetup
  </li>
</ul>
`;

These snapshots will be used as references for matching with future renders, each time we re-run our tests. The tests will pass if the newly created snapshots match these references.

Snapshot tests pass

Addressing failed snapshot tests

Let’s go ahead and update the Tasks view with a feature to strike out the task detail text, if the associated done boolean value is true. We will modify the Tasks component and add the conditional CSS style to the li element as highlighted in the code below:

/* in Tasks.js */
...
 {tasks.map((task, index) =>
   <li key = {index}
       style = {{textDecoration: task.done ? 'line-through' : 'none'}}>    
  {task.detail}
   </li>
 )}
...

When we run the tests again, the reference snapshot generated earlier, for the test case that checks how the component renders with tasks, will no longer match the newly generated snapshot because it now contains the style attribute for each list item. This will fail the test.

Snapshot tests fail

In this specific case, the changes are expected, so we need to update the reference snapshot and ensure future comparisons are made against this new version.

Update options

This can be done easily in the interactive test-runner console by entering u , prompting Jest to update the snapshot reference and then re-run the tests, which will now pass.

Avoiding snapshot testing pitfalls

From the example we discussed above, note that the task of determining whether the snapshot test is failing because code is broken or the reference snapshot needs updating, is left to the developer. This can get problematic if the developer is unfamiliar with the code being tested or if the code is not well-written, and specially if the snapshot test cases are general, do not capture specific developer intent, and the reference snapshot originally generated, itself contains errors.

Hence, it is wise to use snapshot testing cautiously and intentionally, making sure the following is considered:

  • Do not rely solely on snapshot testing for the entire codebase, instead complement and use with other testing techniques
  • Intentionally make snapshots focused, and try to avoid large snapshots to ensure better readability (consider using inline snapshots)
  • Ensure generated snapshots are handling platform-specific data like file paths or generated data like dates (custom serializers and property matchers can be useful in these cases)

Identifying use cases for effective snapshot testing

In order to write effective snapshot tests, it is important to recognize which aspects of our codebase will benefit the most from this kind of testing. Some of the use cases, where we can consider snapshot testing, include scenarios where we want to:

  • Differentiate UI rendering results: We can use snapshots to distinguish between the different UI states, and capture the intention of rendering a UI component in a specific way, ensuring it is not affected when code is updated.
  • Test legacy or existing code: Snapshots can be used to keep checks on how the code is behaving, rather than how it is meant to behave. Keeping snapshot references of existing legacy code behavior can speed up the workflow of refactoring code, making changes and feature upgrades.
  • Test integration regression: We can write snapshot tests of complex UI components to ensure future changes in shared components don’t break intended behavior in a previously verified integration.

Snapshot testing can be a handy tool in our testing arsenal. It can be useful to ensure intended and defined behavior is not changing unexpectedly. It can also make hard-to-define cases easy to test. But the decision to use snapshot testing for your projects, and defining the purpose behind each snapshot test case should be intentional, in order to make snapshot testing effective and actually useful.