A big part of my job is writing “custom tag configurations” for my company’s clients. My company’s product is a “tag,” or a script that runs custom logic when it’s added to a client’s website. Since every website is different, our tag doesn’t always work perfectly out of the box, or by default, for every client. That’s why we’ll customize it to optimize for the location of various objects on their site.
But every once in a while, we’ll find a consistent pattern over several sites that would allow us to deploy the same tag configuration with equal effectiveness. Something like that is a huge win because it means we can service two or more clients while only having to spend time writing one configuration. In this case, we would write a “preset” that could be applied to any company through our UI. Presets carry more weight than custom configurations because if they break, our product will stop working for a series of clients instead of just one. As you might expect, testing for presets is more robust than testing for custom configurations.
This week, I had to make a change to a preset I had written, but when I tried to push my code, I saw that it was now failing a test. It turned out my new configuration wasn’t returning the correct snapshot, so if I wanted to make my change, I’d have to learn what that meant and how to fix it.
Jest
The first thing you’ll read on jestjs.io is that
Jest is a delightful JavaScript Testing Framework with a focus on simplicity.
And indeed, Jest is a framework we can use to test our JavaScript. Much like React, Jest was built and is maintained by Facebook. And so, as you might expect, there’s plenty of emphasis on how well it works with React. Since React is a framework that primarily builds dynamic user interfaces, it provides a unique challenge of how to test interactive displays.
A traditional method that still has its advantages is to actually render a page, interact with it, and see what happens as part of your automated test. While this can be thorough, it is also time-consuming, even when automated. What’s worse, variability can make tests flaky, failing inconsistently when nothing in the code has changed. Dealing with this experience, the folks at Jest created a test based on snapshots.
A Jest Snapshot
The concept of a snapshot isn’t unique to Jest. For example, we could augment the previously mentioned test which renders a page by taking a digital “snapshot” of a rendered page and compared that to the page that renders whenever we try to push out a new change. But that still suffers from the issues of having to run most or all of your app whenever you have to test.
In their documentation, the authors of Jest say that instead of rendering the graphical UI, which would require building the entire app, you can use a test renderer to quickly generate a serializable value for your React tree.
In other words, we can save a component’s JSX as a string and then regenerate that component whenever we want to test. Here’s the example React that the documentation provides:
import React from 'react';
import renderer from 'react-test-renderer';
import Link from '../Link.react';it('renders correctly', () => {
const tree = renderer
.create(<Link page="http://www.facebook.com">Facebook</Link>)
.toJSON();
expect(tree).toMatchSnapshot();
});
This assumes some knowledge of Jest syntax, but since it’s got a focus on simplicity, it’s not too difficult for us to follow. Everything is wrapped in an it function, which essentially defines our test. The function accepts two arguments, a string which is the name of the test and an anonymous function. That second argument sets a variable called tree that uses an imported object called renderer to run a create function.
I haven’t seen the code behind renderer, but I think it’s safe to assume that it can be used to run JSX. We’re passing a Link component to the create function and them immediately converting the result to JSON, which will stringify it. What does that string look like? We can see it in the Jest document right after this snippet:
exports[`renders correctly 1`] = `
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;
It’s easy to miss the tick marks, but they indicate that everything we’re seeing here, from <a to </a> is a string. And if we go back to the last line of our sample test:
expect(tree).toMatchSnapshot();
Our expectation is that the JSON conversion of our component “matches” the snapshot that’s generated when we first run Jest. Of course, the very first time we do this, they’ll always match, even if there’s a problem with the initial code. But if we feel good about our initial component, we’ll have a saved version of that which we can compare to all future iterations.
Advantages and Disadvantages
The advantages to this methodology are clear: it’s faster and more reliable than having to actually run our app, and our snapshot is pretty lightweight too. In a blog post linked in the Jest documentation, Ben McCormick shares a few shortcomings of the process that are also important to recognize:
- Compared to assertion-based tests, snapshot testing is better for testing behavior that will change often, like where elements fit on our page as we develop it. If the behavior is expected to remain static over time, a classic assertion might be better, since we can describe it ourselves and it’s not so easy to update the test (more on updating snapshots later).
- Snapshot tests won’t provide guidance when they fail. Instead, they just tell us that the string generated from our code doesn’t match the snapshot. Sometimes this is intentional and sometimes our subject is so complicated that a simple “this is different” doesn’t get us any closer to identifying why.
- Ease of updating means we can more easily add a mistake to our code. There’s a single-line command to automatically update our Jest snapshots. Even better, when our tests fail, we’re given the option to make a selection that will update the snapshots in that moment. McCormick argues that this necessitates extra-thorough code review so that we don’t absent-mindedly update our snapshot when we instead should have refactored our code.
Generating my Snapshot
Jest provides pretty clear messaging on its tests, so I knew that my change was failing because my snapshot was out of date. As I mentioned, Jest provides a very easy way to address that issue:
jest --updateSnapshot~ OR ~jest -u
But in practice, my code is in a big monorepo with a bunch of different test files and snapshots, most of which have nothing to do with my code at all. I’ll be in a very bad situation if I mistakenly update one of those, so I have to be more specific with my command.
If I want to see what commands I can run within my micro front end (MFE), I can check the package.json file. The file mapped a command test with jest, so I learned that I could run test -u to update my snapshot. I can specify a workspace in yarn and then add the test command, like so:
yarn workspace @my-chosen-workspace test -u
That last line was actually the main thing I learned at work this week, but I wanted to be able to better understand what a snapshot is and why it’s important. I feel like I’ve been hearing about testing forever, but it’s not an easy muscle to flex, especially when you’re iterating on existing code rather than starting from scratch. I’m not sure how much closer I am to writing a suite of Jest tests, but at least I understand the concept just a little bit better than I did before.
Sources
- Jest
- Snapshot Testing, Jest
- Jest 14.0: React Tree Snapshot Testing, Jest
- Testing with Jest Snapshots: First Impressions, Ben McCormick
- Use Jest’s Snapshot Testing Feature: Kent C. Dodds