Improved snapshot testing in Jest with alpha-serializer

Published

Jest test runner has great feature for snapshot testing. A concept that greatly improves the process of writing tests. I'd like to briefly explain how it works and what you can do to improve even more.

How snapshot testing work

Let's start with an example

function testSubject() {
    return {
        foo: 'bar',
        number: 20,
        avast: 'test'
    }
}

describe('foo', () => {
    it('example', () => {
        expect(testSubject()).toMatchSnapshot();
    });
});

Once you run the test for the first time, jest checks whether a snapshot for given test exist. If not then snapshot is created with current result, otherwise compares saved data with current result.

Example snapshot

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`foo example 1`] = `
Object {
  "avast": "test",
  "foo": "bar",
  "number": 20,
}
`;

As you see jest simply serializes the data in a readable form and saves it on a disk. pretty-format it a tool used by jest for serialization and by default is able to handle a lot of types such as dates, functions, regexps etc.

Custom types

As your application grows you might need decent tool for symmetric serialization like alpha-serializer to handle custom types that might not be well supported by pretty-format such as custom errors.

class ValidationError extends Error {
    constructor(message: string, readonly errors: { [key: string]: string[] }) {
        super(message);
        this.message = message;
        this.name = 'ValidationError';
    }
}

describe('foo', () => {
    it('example', () => {
        expect(testSubject()).toMatchSnapshot();
    });

    it('unsupported types', () => {
        expect({
            validationError: new ValidationError('invalid data', {
                name: ['cannot be blank'] // this will be ignored in assertion as they're not properly serialized
            })
        }).toMatchSnapshot();
    });
});

// snapshot result
exports[`foo unsupported types 1`] = `
Object {
  "validationError": [ValidationError: invalid data],
}
`;

Note that property errors does not exist in the snapshot and that will cause the test to pass incorrectly even though the errors property might be different.

In order to fix that we can use alpha-serializer to customize process of serialization for certain types.

import {normalizer, Serializable, serialize} from 'alpha-serializer';
expect.addSnapshotSerializer({
    test(value: any) {
        // check if given type has defined normalization
        return normalizer.hasNormalization(value);
    },
    print(val: any, serializer) {
        // normalize data before serialization
        return serializer(normalizer.normalize(val))
        // you might also use serialization from alpha-serializer
        // return serialize(val)
    }
});

@Serializable({
    normalizer(error: ValidationError) {
        return {message: error.message, errors: error.errors};
    }
})
class ValidationError extends Error {
    constructor(message, readonly errors) {
        super(message);
        this.message = message;
        this.name = 'ValidationError';
    }
}

Run your test again and compare the saved snapshot

exports[`foo unsupported types 1`] = `
Object {
  "validationError": Object {
    "@type": "ValidationError",
    "value": Object {
      "errors": Object {
        "name": Array [
          "cannot be blank",
        ],
      },
      "message": "invalid data",
    },
  },
}
`;

You can also use alpha-serializer-jest that makes whole setup for you.

Now your custom types are handled properly :)

Łukasz Kużyński - Wookieb
Thoughts, tips about programming and related subjects to make your job easier and more pleasant so you won't burnout quickly.
Copyright © Łukasz Kużyński - Wookieb 2023 • All rights reserved.