Ingo Richter

6 minute read

Let’s start with a bold statement:

We all love to write unit great tests for our code. More or less.

Unknown Programmer

Writing unit tests for my code mostly follows this pattern

  1. Write a test and make it fail (red)
  2. Write the function to fix the test (implement function)
  3. Start over with step 1

For one of my projects I was using jest. It’s fast now and it has several features that I highly value. Most of them integrated code coverage and Snapshot testing.

Snapshot testing? What’s that?

When I first saw the part about Snapshot testing, I wasn’t really interested. Okay, it was more that I thought, well, I don’t see a big advantage here over the traditional approach of testing my code. I’m calling functions and make sure that the result of those functions matches my expectations. That’s pretty simple with a function that returns a simple result

1 function add(a, b) {
2     return a + b;
3 };
4 
5 test("Verify that 1 + 3 equals 4", () => {
6     expect(add(1, 3)).toBe(4);
7 });

This is simple and doesn’t require much work.

How about this?

 1 function createTodoItem(subject, projects, contexts, due) {
 2     return {
 3         "subject": subject,
 4         "projects": projects,
 5         "contexts": contexts,
 6         "due": due,
 7         "completed": false,
 8         "archived": false,
 9         "isPriority": false
10     }
11 };
12 
13 test("Verify that new todo item has all required fields", () => {
14     const newTodoItem = createTodoItem("New Task", ["blog"], ["learn", "programming"], "2017-04-17");
15     const expectedTodoItem = {}
16 
17     expect(newTodoItem).toEqual(expectedTodoItem);
18 });

As expected, the output is telling me, that there is something missing. Yes, I didn’t write yet the expected object to compare the result with. I’m too lazy and I always want to avoid writing it. What I’m doing instead, is to call the function, copy the result and create my expected result object with this data. But for now, I’m not going to do it. Let’s see, how Snapshot testing will help solve this task.

Verify that new todo item has all required fields

    expect(received).toEqual(expected)

    Expected value to equal:
      {}
    Received:
      {"archived": false, "completed": false, "contexts": ["learn", "programming"], "due": "2017-04-17", "isPriority": false, "projects": ["blog"], "subject": "New Task"}

    Difference:

    - Expected
    + Received

    -Object {}
    +Object {
    +  "archived": false,
    +  "completed": false,
    +  "contexts": Array [
    +    "learn",
    +    "programming",
    +  ],
    +  "due": "2017-04-17",
    +  "isPriority": false,
    +  "projects": Array [
    +    "blog",
    +  ],
    +  "subject": "New Task",
    +}

      at Object.<anonymous>.test (__tests__/newTask-test.js:7:25)
      at process._tickCallback (internal/process/next_tick.js:109:7)

 PASS  __tests__/add-test.js

Test Suites: 1 failed, 1 passed, 2 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        1.042s
Ran all test suites.

Yeah, I was lazy and didn’t populate expectedTodoItem with all the fields I was expecting. This is the point, where Snapshot testing comes into play. It helps me lazy programmer to avoid writing uneccessary code only to verify my assumptions about the outcome of that function.

 1 function createTodoItem(subject, projects, contexts, due) {
 2     return {
 3         "subject": subject,
 4         "projects": projects,
 5         "contexts": contexts,
 6         "due": due,
 7         "completed": false,
 8         "archived": false,
 9         "isPriority": false
10     }
11 };
12 
13 test("Verify that new todo item has all required fields", () => {
14     const newTodoItem = createTodoItem("New Task", ["blog"], ["learn", "programming"], "2017-04-17");
15 
16     expect(newTodoItem).toMatchSnapshot();
17 });

And here the result

 PASS  __tests__/newTask-test.js
 PASS  __tests__/add-test.js

Test Suites: 2 passed, 2 total
Tests:       2 passed, 2 total
Snapshots:   1 passed, 1 total
Time:        0.816s, estimated 1s
Ran all test suites.

Three notable things

  1. I still didn’t provide a full fledged object that matches my expectation
  2. toMatchSnapshot() was the only code change in the test
  3. The number of Snapshots in the status output of Jest is now 1!

What happened? Jest was taking a Snapshot for that test and was happy with the result. Under the hood, Jest created a __snapshots__ directory in my __tests__ directory and saved the output of the test result. The file is named after the file containing the test. In this case it’s newTask-test.js.snap. Here are the contents of that Snapshot file.

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

exports[`Verify that new todo item has all required fields 1`] = `
Object {
  "archived": false,
  "completed": false,
  "contexts": Array [
    "learn",
    "programming",
  ],
  "due": "2017-04-17",
  "isPriority": false,
  "projects": Array [
    "blog",
  ],
  "subject": "New Task",
}
`;

The key of the exports object is the name of the test itself. The value of it is the result from createTodoItem("New Task", ["blog"], ["learn", "programming"], "2017-04-17"). That is great. I didn’t have to type a line of code for the expected result of this function. By doing a quick visual inspection of the output, I can confirm that this is the expected output.

Now it gets interesting. I’m going to change the return object of the function. The result object will have a new key completedDate and the completed flag will be removed. Let’s see how Jest handles the situation without making any change to the existing, and currently passing, test.

 FAIL  __tests__/newTask-test.js
   Verify that new todo item has all required fields

    expect(value).toMatchSnapshot()

    Received value does not match stored snapshot 1.

    - Snapshot
    + Received

    @@ -1,8 +1,8 @@
     Object {
       "archived": false,
    -  "completed": false,
    +  "completedDate": "",
       "contexts": Array [
         "learn",
         "programming",
       ],
       "due": "2017-04-17",

      at Object.<anonymous>.test (__tests__/newTask-test.js:8:25)
      at process._tickCallback (internal/process/next_tick.js:109:7)

 PASS  __tests__/add-test.js

Snapshot Summary
  1 snapshot test failed in 1 test suite. Inspect your code changes or run with `npm test -- -u` to update them.

Test Suites: 1 failed, 1 passed, 2 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   1 failed, 1 total
Time:        1.113s
Ran all test suites.

The output of the test run informs me, that the Received value does not match stored snapshot 1. That is correct and it was expected. But more importantly, Jest tells me to inspect my code changes or run the tests again with a specific flag to update the snapshot. Since it was an intentional change, I’m going to run the test again with npm test -- -u to update the Snapshot.

> jest "-u"

 PASS  __tests__/newTask-test.js
 PASS  __tests__/add-test.js

Snapshot Summary
  1 snapshot updated in 1 test suite.

Test Suites: 2 passed, 2 total
Tests:       2 passed, 2 total
Snapshots:   1 updated, 1 total
Time:        1.115s
Ran all test suites.

The Snapshot got updated and reflects the current implementation of my function. Well done! No code written to create the expected result object. This is where Snapshot testing is really awesome.

Summary

I hope, that this example helped to understand the Snapshot testing capability of Jest. IMHO, this approach is great for two reasons:

  1. I don’t have to write code to compare complex result objects
  2. Less test code to write, easier to read, more time to work on features

Snapshot testing might not be necessary/wanted for all kind of tests. You might choose the traditiional way for simpler functions. To verify complex result objects, this is an efficient way for testing. Having to write less code is always a great way to improve efficiency.

Don’t hesitate to contact me, if you have questions or suggestions. You can leave a comment below or find me on the social networks mentioned at the top of this post.

Thank you very much for reading!

comments powered by Disqus