Abusing Jest snapshot tests: some nice use-cases 📸
Tags: javascript, testing, snapshots, jest
Published: 2018-08-10 13:48:10

---
title: Abusing Jest snapshot tests: some nice use-cases 📸
published: true
tags: JavaScript, testing, snapshots, jest
canonical_url: https://codewithhugo.com/abusing-jest-snapshot-tests-some-nice-use-cases-/
---

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:

- 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 `npx jest -u` and all snapshots get updated.

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 Jest (which locks you into that ecosystem)


That makes it particularly well-suited for a couple of areas:

- [Config 🎛](#config)
- [Database Models 🏬](#database-models)
  - [Setup 🏗](#setup)
  - [Queries 🔍](#queries)
- [pug or handlebars templates](#pug-or-handlebars-templates)
- [Gotchas of snapshot testing ⚠️](#gotchas-of-snapshot-testing)
  - [Some things (like functions) don’t serialise nicely 🔢](#some-things-like-functions-don-t-serialise-nicely)
  - [If you can do a full match, do it](#if-you-can-do-a-full-match-do-it)
  - [Try to cover the same code/feature with another type of test ✌️](#try-to-cover-the-same-codefeature-with-another-type-of-test)

Full code is available at [github.com/HugoDF/snapshot-everything](https://github.com/HugoDF/snapshot-everything).

> This was sent out on the [Code with Hugo newsletter](https://buttondown.email/hugo) last Monday.[Subscribe](https://buttondown.email/hugo) to get the latest posts right in your inbox (before anyone else).

## Config 🎛

`monitor-queues.test.js`:

```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`:

```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:

```js
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 🏗

```js
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`:

```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:

```js
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`:

```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`:

```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:

```js
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`:

```pug
section
  h1= myTitle
  p= myText
```

`template.handlebars`:

```html
<section>
  <h1>{{ myTitle }}</h1>
  <p>{{ myText }}</p>
</section>
```

`template.test.js`:

```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:

```js
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`:

```json
"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`):

```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 `/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](https://github.com/HugoDF/snapshot-everything).

[Ben Sauer](https://unsplash.com/@sauer?utm_medium=referral&utm_campaign=photographer-credit&utm_content=creditBadge "Download free do whatever you want high-resolution photos from Ben Sauer")