Ben Sauer on Unsplash There’s some nice use-cases for tests outside of the well-travelled React/Vue UI component ones. snapshot 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: the match data is stored in a separate file so it’s harder to lose track of things, eg. being skimmed over during review it’s a lot less effort to change than inline data matching, just run and all snapshots get updated. npx jest -u The following cons also come to mind: it’s a lost less effort to change than inline data matching, which means people need to pay attention to changes in snapshot files despite community efforts, the only major test library that supports out of the box is (which locks you into that ecosystem) Jest That makes it particularly well-suited for a couple of areas: Config 🎛 Database Models 🏬 pug or handlebars templates Gotchas of snapshot testing ⚠️ 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). Config 🎛 : 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,},]`; Database Models 🏬 Setup 🏗 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 {},},]`; Queries 🔍 : 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",],]`; pug or handlebars templates 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>"`; Gotchas of snapshot testing ⚠️ Some things (like functions) don’t serialise nicely 🔢 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 actually the correct function, ( ): is my-model.test.js expect(MockModel.init.mock.calls[0][1].hooks.afterCreate).toBe(MockModel.afterCreate); If you can do a full match, do it 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). Try to cover the same code/feature with another type of test ✌️ Whether that’s a manual smoke test that actually loading up the Bull Arena queue monitoring, or integration tests over the whole app, you should still check that things work 🙂. /arena is 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.