Then snapshot (aka Characterized or Golden Master) testing approach is nothing more as keeping of the system state in "diffable" way, so if an intentional or unintentional change occurs, the test fails and showing the difference of expected and actual output.

Because of that, snapshot testing is convenient to deal with things not directly related to business logic, but rather to representation (or state) of the system. It could be HTML, XML, JSON or any other applicable format.

Instead of writing numerous asserts functions it’s better to test manually (or say, visually) and if the state is right, freeze it (turn it to snapshot). All upcoming deviations would be considered as problems which would fail the test. You can then either fix the problem or confirm that new behavior is right and approve current snapshot.

All snapshots are stored near to your code, under version control and became an integral part of implementation itself.

Not only React components

Event if snapshot based testing re-gained its popularity with React / Jest front-end framework, the application of the approach is much broader and can be applied in many other unusual cases.

We use snapshots to test API's endpoints typically responding JSON, but also HTML and XLSX formats.

Imagine that you can test complex reporting endpoint. Instead of,

expect(response.data.headline).toEqual('Document #12-1332-11'); expect(response.data.address.city).toEqual('Berlin'); expect(response.data[0].line[0].total).toEqual(101); expect(response.data[0].line[1].total).toEqual(102); expect(response.data[0].line[2].total).toEqual(103); expect(response.data[0].line[3].total).toEqual(104); expect(response.data[1].line[0].total).toEqual(105); expect(response.data[1].line[1].total).toEqual(106); expect(response.data[1].line[2].total).toEqual(107); expect(response.data[1].line[3].total).toEqual(108); // and hundred of lines like this

You can do,

expect(response).toMatchSnapshot(); 

Cool, isn’t it? Very cool, but there is a small disclaimer.

The format matters

The better response could be serialized for diffing, the better quality of tests you would have. It means, then the test fails you should have clear understanding of what went wrong. Sometimes, it's not directly possible, so your tests have to take care about it.

Frameworks like Jest and Enzyme taking care of that by serializing React components into JSON, well formated for diffing.

Not only Jest

Right after the React 0.13 release, Facebook delivered a testing framework named Jest designed especially for React needs. With Jest, you have "batteries-included" set of tools, one of each is snapshot testing.

I don’t mind Jest, but there is a problem with it. It’s too opinionated. Which is good for new projects, but what to do for an existing project with already established setup?

In our case, we stick to Mocha and M. Jacksons Expect libraries as our tools of choice. The good thing, you can have Jest testing benefits, without actually using it.

Dissecting Jest, a little

With a great advice from my colleague, @CanGoektas I realized that design of Jest is pretty much composed of different packages. One of the packages is jest-snapshot available as standalone npm package.

The package is rather small to understand details of its work. It exposes the function toMatchSnapshot() which takes entity for snapshotting. It delegates the actual matching to another object, called snapshotState and returns an object with information about did it pass or not as well as extended report.

After checking the tests for the package, I realized it’s not a big deal to use the package alone without using Jest itself.

So, I’ve came up with such code:

import { SnapshotState, toMatchSnapshot } from 'jest-snapshot'; export function toMatchSnapshot(actual, testFile, testTitle) { // Intilize the SnapshotState, it's responsible for actually matching // actual snapshot with expected one and storing results to __snapshots__ folder const snapshotState = new SnapshotState(testFile, { updateSnapshot: process.env.SNAPSHOT_UPDATE ? 'all' : 'new', }); // Bind the toMatchSnapshot to the object with snapshotState and // currentTest name, as toMatchSnapshot expects it as it's this // object members const matcher = toMatchSnapshot.bind({ snapshotState, currentTestName: testTitle, }); // Execute the matcher const result = matcher(actual); // Store the state of snapshot, depending on updateSnapshot value snapshotState.save(); // Return results outside return result;
}

There is a function with such parameters actual, testFile and testTitle which are not currently initialized, but we deal with them further.

Extending Expect

Now, we need to extend expect to support toMatchSnapshot() method. Expect is quite easily extendable via,

expect.extend({}); 

So, what we need to do is,

expect.extend({ toMatchSnapshot() { // TODO: init testFile, testTitle const result = toMatchSnapshot(this.actual, testFile, testTitle); expect.assert(result.pass, !result.pass ? result.report() : ''); return this; }
});

Depending on results returned by toMatchSnapshot it will either fail or pass the assertion.

Here comes Mocha

It’s almost there, but as you can see from function above we are missing couple of variables testFile, testTitle. testFile points to the path where the test file is, so SnapshotState can create snapshots folder near by. testTitle is used as unique index in snapshot storage, so it can be easily extracted for comparison.

We can initialize both variables by asking Mocha. So, finally, I decided to put the Mocha instance as context for toMatchSnapshot() of expect.

expect.extend({ toMatchSnapshot(ctx) { if (!ctx || !ctx.test) { throw new Error( dedent(missing \ctx` parameter for .toMatchSnapshot(), did you forget to pass `this` expect().toMatchSnapshot(this)?`), ); } const { test } = ctx; // would contain the full path to test file const testFile = test.file; const testTitle = makeTestTitle(test); const result = toMatchSnapshot(this.actual, testFile, testTitle); expect.assert(result.pass, !result.pass ? result.report() : ''); return this; }
});

Where makeTestTitle is a simple (and quite ugly) function, that combines the name of all compound context.

export const makeTestTitle = test => { let next = test; const title = []; for (;;) { if (!next.parent) { break; } title.push(next.title); next = next.parent; } return title.reverse().join(' ');
};

So, in case of test like,

describe('GET /v2/invoices/{id}.html specs', function () { describe('de', function () { before(function () { // call API and store response.. }); it('should return correct html', function () { expect(beautify(this.response.payload)).toMatchSnapshot(this); }); });
});

For test 1 it would produce "GET /v2/invoices/{id} specs de should return correct html", good enough.

Finally, you can see I'm using beautify function. As I mentioned above, the way you prepare response for snapshoting and diffing is very important to a quality of results. If an endpoint responds with ugly HTML you can understand nothing from a diff result. Instead of helping you such test will be nothing more as frustration and eventually would be disabled.

Conclusions

With great help of jest-snapshot package, it’s quite easy to integrate snapshot testing to pretty much any infrastructure. Whatever test or assertion framework you use.

I showed an example we approached the problem, of testing HTML resulting endpoint where snapshots are particularly shining. The full example of such tests you can find this gist.

My initial idea was to package the code to a separate npm package, but then I realized it's to much coupled with particular framework such as Expect or Mocha. Instead, I saw that it's better to understand the way jest-snapshot can be used independently, so you have a freedom to adopt it in any project you work.

Hope it helps!