Picking snapshot library

How to pick the right JavaScript snapshot testing library.

Snapshot testing is a great way to remove boilerplate from the unit tests. There are several choices (including some I wrote) for snapshot testing libraries. Here is how to pick the right one depending on the data under test.

If you use Jest or Ava

If you use Jest or Ava testing framework, just use the built-in matcher. You need an exact match in order to pass. See Ava docs for example

1
2
3
4
5
import test from 'ava'
test('my test', t => {
// snapshot file has {foo: 42}
t.snapshot({foo: 42}) // pass
})

Good. Works with text and object snapshots and the runner gives a very friendly error message if there is a mismatch.

If you have another test runner

If you are using Mocha, QUnit or even Jest and Ava you can still use my snap-shot 3rd party library to match objects and strings intelligently.

1
2
3
4
5
6
// Mocha test for example
import snapshot from 'snap-shot'
it('my test', () => {
// snapshot file has {foo: 42}
snapshot({foo: 42}) // pass
})

If case a value is different from the saved snapshot object, the error message shows the difference

Object difference reported using 'variable-diff'

Similarly, a difference in string snapshot is reported using diff-like syntax

Text difference is reported using 'disparity'

You might benefit from learning how snap-shot is implemented from these slides.

If your data is dynamic

Imagine an API returning best selling store item. The item might be different every day or even every minute. So we cannot save the item itself as a snapshot. What we might want to save instead is its schema. As long as the new test value matches the expected schema, things are good. The schema is computed automatically from the first example (just like a snapshot is saved the first time the test runs). In schema-shot this looks like this.

1
2
3
4
5
const schemaShot = require('schema-shot')
it('returns most popular item', () => {
const top = api.getMostPopularItem()
schemaShot(top)
})

The saved snapshot file probably looks like this

1
2
3
4
5
6
7
8
9
10
11
exports['returns most popular item 1'] = {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"id": {
"type": "string",
"required": true
}
},
"additionalProperties": false
}

The most popular item is an object with a single string "id" property. As long as the next popular item has it, it passes. If the property is missing, or there are additional ones, it fails.

1
2
3
4
5
6
const schemaShot = require('schema-shot')
it('returns most popular item', () => {
const top = api.getMostPopularItem()
// top is {id: '401aff'}
schemaShot(top) // pass
})
1
2
3
4
5
6
const schemaShot = require('schema-shot')
it('returns most popular item', () => {
const top = api.getMostPopularItem()
// top is {uuid: '401aff', name: 'my item'}
schemaShot(top) // EXCEPTION: missing "id", new property "name"
})

Schema shot testing is helpful for dynamic data, where the shape of the data is known, but not the actual value.

If your data is growing

Imagine an API returning list of Oscar-winning movies. Every year, the list of "Best Movie" winners is expanded by one item. A saved snapshot might NOT match the new result exactly, but it has to be a subset. This is what subset-shot does for you - it just checks if the saved snapshot is a subset or exact match of the current value.

1
2
3
4
5
6
// snapshot file is [2, 3, 5, 7]
const subsetShot = require('subset-shot')
it('computes primes', () => {
const computedPrimes = [2, 3, 5, 7, 11, 13]
subsetShot(computedPrimes) // pass
})

Same approach works with objects, even nested ones - as long as the new test value has the originally saved snapshot as its subset the test passes.

1
2
3
4
// snapshot is {foo: 42}
it('has object', () => {
subsetShot({foo: 42, bar: 'baz'}) // pass
})

If you have a lot of test cases

If you are testing the same function by providing multiple "given this argument expect this result" you might want to give sazerac a look.

1
2
3
4
5
6
import { test, given } from 'sazerac'
test(isPrime, () => {
given(2).expect(true)
given(3).expect(true)
given(4).expect(false)
})

I have implemented a version of data-driven testing inspired by "sazerac" in snap-shot. Just provide the function and inputs and the snapshot will be generated.

1
2
3
4
5
// Mocha for example
import snapshot from 'snap-shot'
it('tests prime', () => {
snapshot(isPrime, 1, 2, 3, 4, 5, 6, 7, 8, 9)
})

The generated snapshot will contain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
exports['tests prime 1'] = {
"name": "isPrime",
"behavior": [
{
"given": 1,
"expect": false
},
{
"given": 2,
"expect": true
},
{
"given": 3,
"expect": true
},
{
"given": 4,
"expect": false
},
{
"given": 5,
"expect": true
},
...
]
}

Data-driven tests with snapshots save a lot of keystrokes.

Conclusion

Although Jest and Ava built-in snapshot testing is great, you might benefit from specific variations described above, depending on your test data. Best of all - these libs snap-shot, schema-shot and subset-shot work with all unit testing frameworks, including Jest and Ava.

PS: The actual snapshot saving is implemented in snap-shot-core repo, making it simple to write snapshot utilities. Can you suggest something else we could have done to simplify real world testing?