I am a huge fan of Jest's Snapshot Testing - it's a powerful but often misused feature of the testing library. In this post, I'd like to give some advice about how to use snapshot testing effectively within your code, and not just for React components!

How Does It Work?

Essentially, any output that you would like to capture in a snapshot uses a special assertion: expect(something).toMatchSnapshot().

The first time that a test with this assertion is run, Jest will generate a snapshot file, that's stored in a __snapshots__ directory. Commit this to source control, as it will be used on subsequent runs of that test, both locally and on CI, to determine whether there has been a change to the contents of that file. if there has, the test fails.

A snapshot is just that - a snapshot in time. The test will fail when the snapshot content changes, but that's not necessarily a bad thing. It may be that you've made a legitimate change and want this recorded in the snapshot. So, you can re-run the tests with an instruction to update the snapshots. This test run will edit the snapshot file, producing a diff that you can supply to the team at code review and capture the change. It's common to hook up the run of jest to the test npm script in package.json, so that testing can be kicked off running npm test, as such:

{
  "scripts": {
    "test": "jest"
  }
}

So to then update the snapshots, run npm test -- -u. If you use Jest in watch mode, then there's an option (i) to update snapshots interactively.

My Tips and Tricks for Snapshotting Effectively

Don't Snapshot Everything

Because toMatchSnapshot is such an easy assertion to use, in the past I've ran into problems where I have captured an entire React component tree in a single snapshot. What this ends up doing is producing a tens-of-lines-long snapshot, which reviewers assume is correct at the point of code review. But later on, it becomes undiffable when there's a change, and because it's capturing such a large portion of code, it may change for unrealted reasons (for instance if a css class name changes).

Any testing technique should provide an unambiguous, actionable signal as to the failure case. Eagerly snapshotting deep component trees, or large JSON structures, and so on, goes against this rule. Be really careful about only capturing what matters for this particular test. Kevlin Henney describes this as testing "precisely and concretely".

The Jest ESLint Plugin has a preconfigured rule to discourage large snapshots. By default this is 50 lines, which is still too big in my opinion, bu this can be changed through config options.

Consider Inline Snapshots for Simple Use Cases

If your snapshot is relatively small it can be more readable to use .toMatchInlineSnapshot. There's two ways to set this up:

  1. If you know what the snapshot is going to be up front, pass it as a string to .toMatchInlineSnapshot
  2. If you want the initial inline snapshot autogenerated for you, call .toMatchInlineSnapshot without any arguments and it will be written into the test file for you.

Use Readable Snapshot Names

By default, the snapshot name is based on the test name, plus an incrementing integer to support multiple snapshots per test. Following standard conventions on good test naming goes a long way here. Consider that when the test is nested within multiple describe blocks, the snapshot name will contain the full context.

Should any further context be needed, it is possible to pass a string as an argument to .toMatchSnapshot(hintString);. This will then be appended to the snapshot test name. Tests don't just exist for behaviour verification - they can be examples on how to use the code. We spend more time reading code than we do writing code, so every little helps when it comes to making tests more readable.

Ensure Snapshots Are Deterministic

In a nutshell, the same inputs should produce the same output, as the purpose of the snapshot is to capture a save point that can be used for later comparison purposes! This may seem easy, but there's always subtleties that can make this tricky.

For instance, anything that uses Date.now() is going to produce a different result on every execution. To make these tests stable, mock out this global function to return a static date:

Date.now = jest.fn(() => 1646402310215); // That value is 22-03-04 at 13:58

This can also be used to mock out other indeterminate functions, such as Math.random(). But, setting a mock with a hardcoded value might be undesireable for some other test cases in your suite. When snapshotting JavaScript objects, Jest can use a Property Matcher syntax to apply some fuzzy matching logic to indeterminate properties. This lets the test run without any static mocks, but also remain deterministic where it matters:

it('uses property matchers for indeterminate properties', () => {
  const item = {
    created: new Date(),
    id: Math.random(),
    tag: 'test'
  });

  expect(item).toMatchSnapshot({
    created: expect.any(Date),
    id: expect.any(Number)
  });
});

Which produces:

exports[`uses property matchers for indeterminate properties 1`] = `
Object {
  "created": Any<Date>,
  "id": Any<Number>,
  "tag": "test",
`;

Note that the non-property matched property tag in the item object is written verbatim to the snapshot.

You Can Add Custom Serializers!

The Jest expect global is configurable. In addition to adding your own custom assertions, it's also possible to add your own custom snapshot serializers! This controls how Jest will translate an incoming piece of data into a textural format to be stored in the snapshot file. Supply a test function to specify the conditions this serializer is used for, and a print function to produce the output.

Here's a really simple example that serializes all strings to lowercase:

expect.addSnapshotSerializer({
  test: value => typeof value === 'string',
  print: value => value.toLowerCase()
});

You cna be really inventive here, formatting arrays as asciidoc tables, for example. Just remember, make sure that whatever output you make produces clean diffs when something changes and helps the developer understand if a change to a snapshot was intended or unintended - so that they can update the snapshot, or fix their code, respectively.

Multiple Snapshots per Test? Try Only Snapshotting the Changes

A common pattern when testing frontend code is to capture the state of a component in a snapshot before and after some interaction. In the spirit of making the changes easy to read and easy to diff, consider introducing the snapshot-diff utility. This provides a snapshotDiff function that accepts before and after arguments, the only outputs what's different between the two!

expect(snapshotDiff(<SayHello name="sam" />, <SayHello name="ewan">)).toMatchSnapshot();

The alternate syntax expect(a).toMatchDiffSnapshot(b) exists, hooking into Jest as a cusom assertion. There's some funky syntax to be aware of if you use custom snapshot serializers, so check out the docs if that applies to you.

Go Forth and Snapshot

Snapshot testing is a great addition to your utility belt! It doesn't need to just be limited to testing React components. As JSON is a nice serializable format, snapshot testing can be a great tool for API contract testingDeveloping the expertise about when to and when not to snapshot test is something that comes with time, but with the tips above, you should be well to understand how to make the most out of snapshot testing without causing too much of a headache.