How snapshot tests matter

10.02.2021
Martin Friedel
jest

When building a frontend, like any piece of software, it is good practice to create an automated suite of tests to repeatedly verify, that everything is still working as intended. For a React frontend there are several mechanisms what kind of tests you can write and run, including: snapshots.

A snapshot is the detailed output of rendering a component to HTML with maybe a few additional infos.

It is easy enough to do, testing your components and views using jest and a few additional helpers like enzyme or react-test-renderer. Such a test looks something like this:

import * as React from 'react'
import * as renderer from 'react-test-renderer'
import ErrorBox, { ErrorBoxProps } from './ErrorBox'

const props: ErrorBoxProps = {
    errors: [
        'Lorem ipsum dolor sit amet',
        'Phasellus rutrum leo',
    ],
    errorTitle: 'TestCategory',
}

describe('ErrorBox Component', () => {
    it('should render correctly', () => {
        const component = renderer.create(<ErrorBox {...props} />)
        expect(component).toMatchSnapshot()
    })

})

So we expect the component which gets rendered to match a snapshot (which is automatically created in and subsequently read from a __snapshots__ directory next to the test file)

This mode of testing sounds all too simple. All it does is make sure, that the rendered output is the same as before - how is this useful?

The power of the snapshot test lies in three effects.

First: freeze a manual test

The snapshot may be a bit cryptic if there is lots of JS objects as props, but it should still be read. By you. This way, what the snapshot represents is a frozen manual test, that everything is still exactly the way you tested it and found it ok.

Changes should be viewed and made sure that they are what you would expect from the code changes you made and that they are error free. Once you have checked that, what you are comitting is in fact an updated manual test again. Just updating the snapshot with no manual check makes them basically useless.

Second: catching side effects

When you have a component that is an agregation of several other components, then a change in one of them might cause problems. So by changing a child component, the parent will either be the same as before (good) or break, because something has changed and needs to be checked (also good).

This is also true of component libraries you use, if they have breaking changes you have to re-check your code in the places where things changed too.

Third: Learning from snapshots

Reading the contents of a snapshot file is of course done to try to find bugs. But at the same time, you also learn about the internals of the components you are using.

React has spawned an ecosystem for truly reusable component libraries like material-ui which provide such a great basis to build applications from powerful building blocks very quickly. But there is a lot of (well crafted) magic going on in that simple button you are using.

Snapshots provide you a window into what is actually going on inside a component, which may help you better understand the interface it has. Many components consist of nested HTML elements and you can pass prop values or assign styles to different elements inside the component. This may sometimes be hard to grasp until you make a snapshot test and look inside to see the hierarchy and where your style may have gone to.

How nothing was a great result

We were once tasked with cleaning up in a project where two teams had worked on a web application. Initially a react savvy frontend dev (this was a few years ago) started off by building a component library of common UI elements from scratch. Then however, one team chose to continue with Typescript, the other went with flow.

We had the dubious pleasure of combining the two copies back into one library that supported both TS and flow. The great thing was that there were snapshot tests! We converted JS to TS and changed class based components to functional ones. In the end when we were done it was great to see what the difference was in the snapshots: Nothing.

Everything was still green.

That was a powerful moment, as much as we had changed - for the rest of the application it was actually the same and we could prove it.

An example

A snapshot file storing the above component rendered with react-test-renderer will look like this:

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

exports[`ErrorBox Component should render correctly 1`] = `
<ul
  className="MuiList-root ErrorBox"
>
  <div
    className="MuiListSubheader-root ErrorBoxSubheader"
  >
    <h6
      className="MuiTypography-root MuiTypography-subtitle1"
    >
      error.title.TestCategory
    </h6>
  </div>
  <div
    className="MuiBox-root MuiBox-root-1"
  >
    <li
      className="MuiListItem-root MuiListItem-gutters"
      disabled={false}
    >
      <div
        className="MuiListItemIcon-root"
      >
        <svg
          aria-hidden={true}
          className="MuiSvgIcon-root"
          focusable="false"
          viewBox="0 0 24 24"
        >
          <circle
            cx="12"
            cy="12"
            r="8"
          />
        </svg>
      </div>
      <div
        className="MuiListItemText-root"
      >
        <p
          className="MuiTypography-root MuiTypography-body1"
        >
          error.message.Lorem ipsum dolor sit amet 
        </p>
      </div>
    </li>
  </div>
  <div
    className="MuiBox-root MuiBox-root-2"
  >
    <li
      className="MuiListItem-root MuiListItem-gutters"
      disabled={false}
    >
      <div
        className="MuiListItemIcon-root"
      >
        <svg
          aria-hidden={true}
          className="MuiSvgIcon-root"
          focusable="false"
          viewBox="0 0 24 24"
        >
          <circle
            cx="12"
            cy="12"
            r="8"
          />
        </svg>
      </div>
      <div
        className="MuiListItemText-root"
      >
        <p
          className="MuiTypography-root MuiTypography-body1"
        >
          error.message.Phasellus rutrum leo
        </p>
      </div>
    </li>
  </div>
</ul>
`;

So what can I see here?

  1. The title and the error messages are not supposed to be the text itself, but i18n identifiers, which is why there is the "error.message.Phasellus rutrum leo" thing.
  2. Oh, look, the ListItem from MaterialUI is actually inserting an SVG as the bullet instead of using a HTML list! Sure, that makes the style more independent of the browser, but also changes how we can style it. We may be tempted to try and style it the wrong way if we did not know that ListItem is not actually an <li> tag.

So do not discard the simple snapshot test too easily, it offers worthwhile benefits in itself. Writing it is easy, reading the snapshot does take a bit of diligence to spot abnormalities, but I claim this takes less than tracing an error back to its source in a larger context.