Adding multi-snapshot testing to Jest
In this post I explain how to monkeypatch Jest’s internal functionality in order to improve the development experience by creating separate snapshot files per test. This is particularly useful for the DOM snapshots which can be quite large and difficult to review in the context of source control.
Snapshot tests are a very useful tool whenever you want to make sure your UI does not change unexpectedly.
Snapshot testing basics
Snapshot testing is a simple concept. You create a test for some permutation of your UI component written with one or another framework, a snapshot file will be saved somewhere in your project per test file. You commit snapshots to source control and every time you (or CI) run tests, there is check for accidental UI changes.
But actually, we can snapshot anything. For the simplicity, we will use a basic example that tries to snapshot numbers 😃 :
Even though 100 is a constant, we still can create a snapshot of it. When running this piece of art with
jest the new snapshot file will be created under the
__snapshots__ directory. The name of this snapshot will be the name of the test (according to our example it will be called
simple_snapshot_1.test.js.snap). Since the snapshot was created from the numerical value, the content of the snapshot file will be rather thin:
exports[`match number in snapshot 1`] = `100`;
If we will change the test now to match another value, for example
200 and will run it against the existing snapshot, the test will fail, since the received value (200) doesn’t match the one stored in a snapshot (100).
Now, let’s create a test that tries to snapshot two numbers:
simple_snapshot_2.test.js.snap file will be created under the
__snapshots__ directory, and the content of this file will contain both numbers:
exports[`match 2 numbers in snapshot 1`] = `150`;
exports[`match 2 numbers in snapshot 2`] = `250`;
Even if we will create a test with a few cases:
There will be a single snapshot file for this test —
simple_snapshot_3.test.js.snap, that will contain all the snapshot values.
Basically, this is what the snapshot matcher (
The problem starts when you have big snapshots. For example you have a test for all permutations of some part of your UI in React. In this case snapshot will contains a big chunk of the markup. This snapshot can grow really fast and you won’t really be a able to track all the changes in it. How will you review it ? Will you want to commit a huge file to git ? 😞
The similar problem raised in Storybook here. Storybook has a very useful addon called StoryShots. If you a Storybook fan like me, you will create stories for mostly all components in your project. When adding StoryShots to the picture, you will win the snapshot automation for all stories you’ve created. But !! StoryShots runs in a scope of one test file. Which means that for the entire project listed in your Storybook will be created an enormously big snapshot file. And here comes a need to allow separation of the snapshots to multiple files.
The thing is that Facebook doesn’t really want to support this kind of behavior because of the implications this brings with it: How to track all these snapshot files created from one test? What happens if you delete test, but there were few snapshots created from it, how will they be cleaned? (You can read the discussions here and here) And actually I agree with their arguments — this behavior may have inconsistent and even unpredictable effects. But there are cases when we don’t need such a strict policy. So let’s do it by ourselves.
To be honest, we don’t want to re-create the whole functionality of snapshot testing by ourselves. This is a great deal of work that was already done by Facebook. Let’s just patch the relevant places to support our needs.
In order to not conflict with the Jest’s names we will call our matcher —
toMatchSpecificSnapshot 😈. The required behavior is to provide a snapshot path to the matcher as an argument. So the following test should look like this:
Since we are creating a custom matcher, we need to register it to be known by Jest:
Now, let’s see a bit how the standard
toMatchSnapshot works in order to understand how to implement our custom
toMatchSpecificSnapshot matcher. The entire snapshot’s logic is located in a dedicated package of the Jest’s monorepo — here. I will not drill down too much to the internal logic of the jest-snapshot package (it took me quite some time to debug and understand it). One of the central places is the
State object. It’s created internally due to the global setup per every test file run and contains the metadata of the snapshot.
Later, when we call to the regular
toMatchSnapshot method from the test, it checks if the received value matches to the one from the snapshot. Note that
snapshotState variable exists in the scope of the test:
So, in order to achieve the required behavior, we need to provide to our custom matcher a new state instance created by us, with the path to our separated snapshot:
Now, we need to call the
toMatchSnapshot function, but in the way it will use ours new state internally 😓… For this purpose we need to monkeypatch
toMatchSnapshot method to be run with the context that contains a new state instance 😰. Sounds confusing, but let’s see:
Looks like everything is done. But when running our new shiny test with the custom matcher — nothing will actually happen. The test is passed but the snapshot is not created.
The thing is that the snapshot file is not really saved once the
toMatchSnapshot is called, but in the end of the test run (because as we said, snapshot is stored per test file, so Jest needs to gather the data from all test cases). Besides that, Jest calls
SnapshotState.save() method of the instance that was created by Jest’s setup. But we have another instance of this state object… We need to call the save method by ourselves (again !). Since our functionality allows to store the same snapshot from different test files and different snapshots from the same test file, we will need to track all the snapshot states instances and call the save method for all of them after the all tests are done:
Now, we achieved our desired behavior and can safely run the test 🤘 :
After all, you may agree with the way this was done, or oppose it, but I hope you learned something new 😉
P.S. This jest plugin will be used in the upcoming release of Storybook (3.3).