Efficient JavaScript Unit Testing with Jest and Snapshots

Make your software testing life easier with Jest and Snapshots to test your javascript code

Ingo Richter

7 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 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 Writing unit tests for my code mostly follows this pattern

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

This is simple and doesn’t require much work.

How about this?

 1function 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
13test("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});
19

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.

 1Verify that new todo item has all required fields
 2
 3    expect(received).toEqual(expected)
 4
 5    Expected value to equal:
 6      {}
 7    Received:
 8      {"archived": false, "completed": false, "contexts": ["learn", "programming"], "due": "2017-04-17", "isPriority": false, "projects": ["blog"], "subject": "New Task"}
 9
10    Difference:
11
12    - Expected
13    + Received
14
15    -Object {}
16    +Object {
17    +  "archived": false,
18    +  "completed": false,
19    +  "contexts": Array [
20    +    "learn",
21    +    "programming",
22    +  ],
23    +  "due": "2017-04-17",
24    +  "isPriority": false,
25    +  "projects": Array [
26    +    "blog",
27    +  ],
28    +  "subject": "New Task",
29    +}
30
31      at Object.<anonymous>.test (__tests__/newTask-test.js:7:25)
32      at process._tickCallback (internal/process/next_tick.js:109:7)
33
34 PASS  __tests__/add-test.js
35
36Test Suites: 1 failed, 1 passed, 2 total
37Tests:       1 failed, 1 passed, 2 total
38Snapshots:   0 total
39Time:        1.042s
40Ran all test suites.
41

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 unnecessary code only to verify my assumptions about the outcome of that function.

 1function 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
13test("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});
18

And here the result.

1PASS  __tests__/newTask-test.js
2PASS  __tests__/add-test.js
3
4Test Suites: 2 passed, 2 total
5Tests:       2 passed, 2 total
6Snapshots:   1 passed, 1 total
7Time:        0.816s, estimated 1s
8Ran all test suites.
9

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.

 1// Jest Snapshot v1, https://goo.gl/fbAQLP
 2
 3exports[`Verify that new todo item has all required fields 1`] = `
 4Object {
 5  "archived": false,
 6  "completed": false,
 7  "contexts": Array [
 8    "learn",
 9    "programming",
10  ],
11  "due": "2017-04-17",
12  "isPriority": false,
13  "projects": Array [
14    "blog",
15  ],
16  "subject": "New Task",
17}
18`;
19

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.

 1 FAIL  __tests__/newTask-test.js
 2   Verify that new todo item has all required fields
 3
 4    expect(value).toMatchSnapshot()
 5
 6    Received value does not match stored snapshot 1.
 7
 8    - Snapshot
 9    + Received
10
11    @@ -1,8 +1,8 @@
12     Object {
13       "archived": false,
14    -  "completed": false,
15    +  "completedDate": "",
16       "contexts": Array [
17         "learn",
18         "programming",
19       ],
20       "due": "2017-04-17",
21
22      at Object.<anonymous>.test (__tests__/newTask-test.js:8:25)
23      at process._tickCallback (internal/process/next_tick.js:109:7)
24
25 PASS  __tests__/add-test.js
26
27Snapshot Summary
28  1 snapshot test failed in 1 test suite. Inspect your code changes or run with `npm test -- -u` to update them.
29
30Test Suites: 1 failed, 1 passed, 2 total
31Tests:       1 failed, 1 passed, 2 total
32Snapshots:   1 failed, 1 total
33Time:        1.113s
34Ran all test suites.
35

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.

 1> jest "-u"
 2
 3 PASS  __tests__/newTask-test.js
 4 PASS  __tests__/add-test.js
 5
 6Snapshot Summary
 7  1 snapshot updated in 1 test suite.
 8
 9Test Suites: 2 passed, 2 total
10Tests:       2 passed, 2 total
11Snapshots:   1 updated, 1 total
12Time:        1.115s
13Ran all test suites.
14

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

Summary

I hope that this example helped to understand the Snapshot testing capability of Jest. IMHO, this approach is excellent 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 traditional 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