Snapshot Testing with Jest

Snapshot Testing with Jest

·

4 min read

Snapshot testing is a new way of test and useful for UI testing specially. Normally for testing UI component, we first render the component and then do a lot of assertions. This is called the assert style. With snapshot testing, the rendered component will be converted into a structure and will be compared to the structure generated previously. Among comparison, we can see if there is some unexpected changes and make a decision on them.

Let's start a new react project to see how it works.

npx create-react-app my-app

Then we have this Link component that needs to be tested.

import { useState } from 'react';

const STATUS = {
    HOVERED: 'hovered',
    NORMAL: 'normal',
};

export default function Link({ page, children }) {
    const [status, setStatus] = useState(STATUS.NORMAL);

    const onMouseEnter = () => {
        setStatus(STATUS.HOVERED);
    };

    const onMouseLeave = () => {
        setStatus(STATUS.NORMAL);
    };

    return (
        <a
            className={status}
            href={page || '#'}
            onMouseEnter={onMouseEnter}
            onMouseLeave={onMouseLeave}
        >
            {children}
        </a>
    );
}

As you can see, it is a simple a element with some event mounted.

Before we go to snapshot testing, let see the normal way. the so-called assert way. Code is like below.

describe("the assert way", () => {
    it('the assert way', () => {

        const name = "123";
        const url = `http://www.${name}.com`;
        render(<Link page={url}>{name}</Link>);

        // get the rendered component
        const link = screen.getByRole('link');

        // assert link content
        expect(link).toHaveTextContent(name);

        // assert link address
        expect(link).toHaveAttribute('href', url);

        // assert hover effect
        userEvent.hover(link);
        expect(link).toHaveAttribute("class", STATUS.HOVERED);

        // assert unhover effect
        userEvent.unhover(link);
        expect(link).toHaveAttribute("class", STATUS.NORMAL);
    });
});

Now let see how to do it with snapshot testing. First we need to install a library.

npm install --save-dev react-test-renderer

Then we can write test code like below.

describe("the snapshot way", () => {

    it('the snapshot way', () => {
        const component = renderer.create(
            <Link page="http://www.123.com">Facebook</Link>,
        );
        let tree = component.toJSON();
        expect(tree).toMatchSnapshot();

        // manually trigger the callback
        renderer.act(() => {
            tree.props.onMouseEnter();
        });
        // re-rendering
        tree = component.toJSON();
        expect(tree).toMatchSnapshot();

        // manually trigger the callback
        renderer.act(() => {
            tree.props.onMouseLeave();
        });
        // re-rendering
        tree = component.toJSON();
        expect(tree).toMatchSnapshot();
    });
});

If we run this code, there is no snapshot so the new snapshot will be created ./__snapshots/Link.test.js.snap. Its content is like below.

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

exports[`the snapshot way the snapshot way 1`] = `
<a
  className="normal"
  href="http://www.123.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}
>
  Facebook
</a>
`;

exports[`the snapshot way the snapshot way 2`] = `
<a
  className="hovered"
  href="http://www.123.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}
>
  Facebook
</a>
`;

exports[`the snapshot way the snapshot way 3`] = `
<a
  className="normal"
  href="http://www.123.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}
>
  Facebook
</a>
`;

Inside this file, there are 3 component structure stored. Back to the source code you will understand, the first one the normal rendered component, the second and third are component after user event.

Later, suppose we do some changes on this component and forget to mount the onMouseEnter event.

<a
    className={status}
    href={page || '#'}
    // onMouseEnter={onMouseEnter}
    onMouseLeave={onMouseLeave}
>
    {children}
</a>

Then the rendered component structure should be different from the last one. So the test should fail.

    - Snapshot  - 1
    + Received  + 0

      <a
        className="normal"
        href="http://www.123.com"
    -   onMouseEnter={[Function]}
        onMouseLeave={[Function]}
      >
        Facebook
      </a>

      37 |         );
      38 |         let tree = component.toJSON();
    > 39 |         expect(tree).toMatchSnapshot();
         |                      ^
      40 |
      41 |         // manually trigger the callback
      42 |         renderer.act(() => {

      at Object.<anonymous> (src/Link.test.js:39:22)
      at TestScheduler.scheduleTests (node_modules/@jest/core/build/TestScheduler.js:333:13)
      at runJest (node_modules/@jest/core/build/runJest.js:404:19)

 › 1 snapshot failed.
Snapshot Summary

We see this difference on snapshots, then we can make a decision: either change the source code to make this test pass, or accept this snapshot and update the stored snapshot files with this one. We could use command jest --updateSnapshot to update all snapshot files, or use flag --testNamePattern to only update the targeted tests.

As you can see, this is a totally different way of testing UI component. We don't need to write all the detailed assertions. All the required information is stored in the snapshot files. But there are also some downsides. In this way, the testing cases are not clear. It will be hard for other developers to follow the expected values. So snapshot testing is not a replacement for normally assert style testing. It is another tool we can use to make tests faster and easier.