Snapshot Testing

Part 4 of 5 in our Testing like a Pro in JavaScript series.
Written by Alex Jover Morales
Snapshot Testing

One of the features that amazed me about Jest is snapshot testing. It’s not necessarily a Jest-only feature, but more of a technique and concept. Anyways, the first time I’ve seen it was in Jest.

Snapshot testing is the technique of asserting by comparing two different outputs. The way it’s done is quite similar to the visual regression testing of the end to end tests.

For example, if you wanna test that a button hasn’t change its styling, you take a base screenshot of a button, and then every time the test suite is run, a new screenshot is taken and it’s compared pixel by pixel with the base screenshot. If they match, the test passes, but if not there could be a regression, depending if that change is intended or not.

Snapshot testing applies that same technique but based on serializable output, such as text or json, instead of images. Thus, it’s a powerful tool for any kind of output-based feature, where rendering web components can be one of the most important as we’ll see later.

In order to illustrate snapshot testing, let’s see a simple example where we have a function that returns an error message, based on its parameter, and throws an error in case it doesn’t match the conditions:

// error.js
export default function getErrorMessage(code) {
  if (code === 1) {
    return "The camel walks on a leg";
  } else if (code === 2) {
    return "Rabbits don't eat carrots";
  } else if (code === 3) {
    return "Cats don't eat mouses";
  }
  throw new Error("No error messages for that code");
}

If we attempt to make a test for it using the tools we learnt so far, we’ll create it similar to the following one:

import getErrorMessage from "./error-message";

describe("getErrorMessage", () => {
  it("returns camel message when code is 1", () => {
    expect(getErrorMessage(1)).toBe("The camel walks on a leg");
  });

  it("returns rabbit message when code is 2", () => {
    expect(getErrorMessage(2)).toBe("Rabbits don't eat carrots");
  });

  it("returns cat message when code is 3", () => {
    expect(getErrorMessage(3)).toBe("Cats don't eat mouses");
  });

  it("throws an error otherwise", () => {
    expect(() => getErrorMessage(4)).toThrow("No error messages for that code");
  });
});

The thing is, probably we don’t care much about the exact error message, we just want it to be one and to be consistent. Then, the current test has a couple of issues:

  • It’s repetitive: it has a test for each case with the same logic.
  • When you change the message on the source code side, you’ll need to change the test. Remember the hint: tests that need to be updated frequently don’t look like good tests.

Using snapshot testing we can avoid these issues while providing the same value to the test. Instead of asserting specific error messages, we just want to check that we get some error messages if we use the code 1, 2 or 3, and an error if we don’t, and we want to keep those errors as they are.

Jest introduces specific matchers for snapshot testing: toMatchSnapshot for serializable values and toThrowErrorMatchingSnapshot for errors thrown. Let’s use them to rewrite the previous test:

import getErrorMessage from "./error-message";

describe("getErrorMessage", () => {
  it("returns an error for a valid code", () => {
    expect(getErrorMessage(1)).toMatchSnapshot();
    expect(getErrorMessage(2)).toMatchSnapshot();
    expect(getErrorMessage(3)).toMatchSnapshot();
  });

  it("throws an error otherwise", () => {
    expect(() => getErrorMessage(4)).toThrowErrorMatchingSnapshot();
  });
});

As you can see, the tests became much simpler, specially the ones for the valid code. They’re checking now that the tests give an expected output based on the input by comparing the snapshots.

Of course, the first time we run it, there are no snapshots. Instead, we’ll see they’ve been created, as the following image shows:

Master Vue.js with Vue School

Top notch Vue.js courses and over 200 lessons for just $12 per month!

Let’s try now to change the value returned when code is equal to 1 in the getErrorMessage function:

if (code === 1) {
  return "The dog walks on a leg";
}

Now you should see the following error, telling you that a snapshot test has failed:

As you can see, it’s giving us the specific error, even pointing to the line of the expect statement that doesn’t match the snapshot.

At this point, there are two paths:

  • You’ve made a mistake and the test is giving you a real error.
  • You’ve made an intentional change to the code, so the snapshot is outdated and you need to update it.

Let’s say in this case we intentionally made the change. In that case, the failing test is not a regression because it is expected to fail and it needs to be updated.

How? If you run the tests in watch mode, you might have noticed in the previous image the following text:

Inspect your code changes or press u to update them.

Just like that, if you press the “u” key, they will be updated. That’s all.

**Warning****: don’t fall into the temptation of quickly pressing “u”, which is a common mistake. Make sure the test is really outdated and check carefully your code.

In order to see all the watch mode options, you can press the “w” key:

In addition to the the “u” option to update all failing snapshots, you have the “i” option to update them one by one selectively.

We’ve seen a pretty simple example, but the cool thing of snapshot testing is that it’s easy to use for more complex examples as well. Remember that it works on any serialisable input which means we can use it to compare complex JSON structures, JavaScript objects and even DOM elements.

Master Vue.js with Vue School

Top notch Vue.js courses and over 200 lessons for just $12 per month!


Article written by Alex Jover Morales

Passionate web developer. Author of Test Vue.js components with Jest on Leanpub. I co-organize Alicante Frontend. Interested in web performance, PWA, the human side of code and wellness. Cat lover, sports practitioner and good friend of his friends. His bike goes with him.