Ben Sauer on Unsplash
There’s some nice use-cases for snapshot tests outside of the well-travelled React/Vue UI component ones.
In other words, although React and Vue testing with snapshots is pretty well documented, that’s not the only place they’re useful.
As a rule of thumb, you could replace a lot of unit tests that assert on with specific data with snapshot tests.
We have the following pros for snapshot tests:
npx jest -u
and all snapshots get updated.The following cons also come to mind:
That makes it particularly well-suited for a couple of areas:
Full code is available at github.com/HugoDF/snapshot-everything.
This was sent out on the Code with Hugo newsletter last Monday.
Subscribe to get the latest posts right in your inbox (before anyone else).
monitor-queues.test.js
:
jest.mock('bull-arena');const { monitorQueues } = require('./monitor-queues');describe('monitorQueues', () => {test('It should return an Arena instance with parsed data from REDIS_URL', () => {const redisPort = 5555;const REDIS_URL = `redis://h:passsssword@hosting:${redisPort}/database-name`;const QUEUE_MONITORING_PATH = '/arena';const ArenaConstructor = require('bull-arena');ArenaConstructor.mockReset();monitorQueues({ REDIS_URL, QUEUE_MONITORING_PATH });expect(ArenaConstructor).toHaveBeenCalledTimes(1);expect(ArenaConstructor.mock.calls[0]).toMatchSnapshot();});test('It should return an Arena instance with defaulted redis data when REDIS_URL is empty', () => {const REDIS_URL = '';const QUEUE_MONITORING_PATH = '/arena';const ArenaConstructor = require('bull-arena');ArenaConstructor.mockReset();monitorQueues({ REDIS_URL, QUEUE_MONITORING_PATH });expect(ArenaConstructor).toHaveBeenCalledTimes(1);expect(ArenaConstructor.mock.calls[0]).toMatchSnapshot();});});
monitor-queues.js
:
const Arena = require('bull-arena');const { JOB_TYPES } = require('./queue/queues');const url = require('url');function getRedisConfig (redisUrl) {const redisConfig = url.parse(redisUrl);return {host: redisConfig.hostname || 'localhost',port: Number(redisConfig.port || 6379),database: (redisConfig.pathname || '/0').substr(1) || '0',password: redisConfig.auth ? redisConfig.auth.split(':')[1] : undefined};}const monitorQueues = ({ REDIS_URL, QUEUE_MONITORING_PATH }) =>Arena({queues: [{name: JOB_TYPES.MY_TYPE,hostId: 'Worker',redis: getRedisConfig(REDIS_URL)}]},{basePath: QUEUE_MONITORING_PATH,disableListen: true});module.exports = {monitorQueues};
Gives the following snapshots:
exports[`monitorQueues It should return an Arena instance with defaulted redis data when REDIS_URL is empty 1`] = `Array [Object {"queues": Array [Object {"hostId": "Worker","name": "MY_TYPE","redis": Object {"database": "0","host": "localhost","password": undefined,"port": 6379,},},],},Object {"basePath": "/arena","disableListen": true,},]`;
exports[`monitorQueues It should return an Arena instance with parsed data from REDIS_URL 1`] = `Array [Object {"queues": Array [Object {"hostId": "Worker","name": "MY_TYPE","redis": Object {"database": "database-name","host": "hosting","password": "passsssword","port": 5555,},},],},Object {"basePath": "/arena","disableListen": true,},]`;
test('It should initialise correctly', () => {class MockModel { }MockModel.init = jest.fn();jest.setMock('sequelize', {Model: MockModel});jest.resetModuleRegistry();const MyModel = require('./my-model');const mockSequelize = {};const mockDataTypes = {UUID: 'UUID',ENUM: jest.fn((...arr) => `ENUM-${arr.join(',')}`),TEXT: 'TEXT',STRING: 'STRING'};MyModel.init(mockSequelize, mockDataTypes);expect(MockModel.init).toHaveBeenCalledTimes(1);expect(MockModel.init.mock.calls[0]).toMatchSnapshot();});
my-model.js
:
const { Model } = require('sequelize');
class MyModel extends Model {static init (sequelize, DataTypes) {return super.init({disputeId: DataTypes.UUID,type: DataTypes.ENUM(...['my', 'enum', 'options']),message: DataTypes.TEXT,updateCreatorId: DataTypes.STRING,reply: DataTypes.TEXT},{sequelize,hooks: {afterCreate: this.afterCreate}});}
static afterCreate() {// do nothing}}
module.exports = MyModel;
Gives us the following snapshot:
exports[`It should initialise correctly 1`] = `Array [Object {"disputeId": "UUID","message": "TEXT","reply": "TEXT","type": "ENUM-my,enum,options","updateCreatorId": "STRING",},Object {"hooks": Object {"afterCreate": [Function],},"sequelize": Object {},},]`;
my-model.test.js
:
jest.mock('sequelize');const MyModel = require('./my-model');
test('It should call model.findOne with correct order clause', () => {const findOneStub = jest.fn();const realFindOne = MyModel.findOne;MyModel.findOne = findOneStub;const mockDb = {Association: 'Association',OtherAssociation: 'OtherAssociation',SecondNestedAssociation: 'SecondNestedAssociation'};MyModel.getSomethingWithNestedStuff('1234', mockDb);expect(findOneStub).toHaveBeenCalled();expect(findOneStub.mock.calls[0][0].order).toMatchSnapshot();MyModel.findOne = realFindOne;});
my-model.js
:
const { Model } = require('sequelize');
class MyModel extends Model {static getSomethingWithNestedStuff(match, db) {return this.findOne({where: { someField: match },attributes: ['id','createdAt','reason'],order: [[db.Association, db.OtherAssociation, 'createdAt', 'ASC']],include: [{model: db.Association,attributes: ['id'],include: [{model: db.OtherAssociation,attributes: ['id','type','createdAt'],include: [{model: db.SecondNestedAssociation,attributes: ['fullUrl', 'previewUrl']}]}]}]});}}
Gives the following snapshot:
exports[`It should call model.findOne with correct order clause 1`] = `Array [Array ["Association","OtherAssociation","createdAt","ASC",],]`;
This is pretty much the same as the Vue/React snapshot testing stuff, but let’s walk through it anyways, we have two equivalent templates in Pug and Handlebars:
template.pug
:
sectionh1= myTitlep= myText
template.handlebars
:
<section><h1>{{ myTitle }}</h1><p>{{ myText }}</p></section>
template.test.js
:
const pug = require('pug');
const renderPug = data => pug.renderFile('./template.pug', data);
test('It should render pug correctly', () => {expect(renderPug({myTitle: 'Pug',myText: 'Pug is great'})).toMatchSnapshot();});
const fs = require('fs');const Handlebars = require('handlebars');const renderHandlebars = Handlebars.compile(fs.readFileSync('./template.handlebars', 'utf-8'));
test('It should render handlebars correctly', () => {expect(renderHandlebars({myTitle: 'Handlebars',myText: 'Handlebars is great'})).toMatchSnapshot();});
The bulk of the work here actually compiling the template to a string with the raw compiler for pug and handlebars.
The snapshots end up being pretty straightforward:
exports[`It should render pug correctly 1`] = `"<section><h1>Pug</h1><p>Pug is great</p></section>"`;
exports[`It should render handlebars correctly 1`] = `"<section><h1>Handlebars</h1><p>Handlebars is great</p></section>"`;
See in __snapshots__/my-model.test.js.snap
:
"hooks": Object {"afterCreate": [Function],},
We should really add a line like the following to test that this function is actually the correct function, (my-model.test.js
):
expect(MockModel.init.mock.calls[0][1].hooks.afterCreate).toBe(MockModel.afterCreate);
A lot of the time, a hard assertion with an object match is a good fit, don’t just take a snapshot because you can.
You should take snapshots for things that pretty much aren’t the core purpose of the code, eg. strings in a rendered template, the DOM structure in a rendered template, configs.
The tradeoff with snapshots is the following:
A snapshot gives you a weaker assertion than an inline
_toBe_
or_toEqual_
does, but it’s also a lot less effort in terms of code typed and information stored in the test (and therefore reduces complexity).
Whether that’s a manual smoke test that /arena
is actually loading up the Bull Arena queue monitoring, or integration tests over the whole app, you should still check that things work 🙂.
Full code is available at github.com/HugoDF/snapshot-everything.
Get all the posts of the week before anyone else in your inbox: Subscribe Now
Originally published at codewithhugo.com on August 10, 2018.