How to Jest Snapshot Test the Difference

Snapshot tests are a common way to write lightweight component tests. When a snapshot test runs for the first time, it stores its output (e.g. rendered component’s HTML structure) in a snapshot output file. Every time the snapshot test runs again, another snapshot output file gets created; which is used to diff the output against the old snapshot test’s output file. If the snapshot’s output has changed, the developer accepts or denies the changes. This way, developers keep an overview of their recent changes.

import React from 'react';

const App = () => {
  const [counter, setCounter] = React.useState(0);

  return (
    <div>
      <h1>My Counter</h1>
      <Counter counter={counter} />

      <button type="button" onClick={() => setCounter(counter + 1)}>
        Increment
      </button>

      <button type="button" onClick={() => setCounter(counter - 1)}>
        Decrement
      </button>
    </div>
  );
};

export const Counter = ({ counter }) => (
  <div>
    <p>{counter}</p>
  </div>
);

export default App;

The code snippet shows a React application that implements a counter which can be increased/decreased with a React Hook by using one of two rendered buttons. A straightforward snapshot test for the React component could be implemented the following way:

import React from 'react';
import renderer from 'react-test-renderer';

import App from './App';

describe('App', () => {
  it('renders', () => {
    const component = renderer.create(<App />);
    let tree = component.toJSON();
    expect(tree).toMatchSnapshot();
  });
});

If one would run the snapshot test, the following snapshot output file would be generated:

exports[`App increments the counter 1`] = `
<div>
  <h1>
    My Counter
  </h1>
  <div>
    <p>
      0
    </p>
  </div>
  <button
    onClick={[Function]}
    type="button"
  >
    Increment
  </button>
  <button
    onClick={[Function]}
    type="button"
  >
    Decrement
  </button>
</div>
`;

That’s the most basic approach for snapshot testing in React. The question for this tutorial: What happens if you want to snapshot test a provoked change of your re-rendered component?

For instance, in the case of our React application, one could invoke one of the two buttons to cause a state change which increases the counter which would lead to a re-render of the component. Afterward, a new snapshot test could be used to assert the differences of the rendered output:

import React from 'react';
import renderer from 'react-test-renderer';

import App from './App';

describe('App', () => {
  it('increments the counter', () => {
    const component = renderer.create(<App />);
    let tree = component.toJSON();
    expect(tree).toMatchSnapshot();

    component.root.findAllByType('button')[0].props.onClick();


    tree = component.toJSON();
    expect(tree).toMatchSnapshot();
  });
});

After running the snapshot test, we would end up with two snapshot outputs in the same snapshot output file. The following code snippet shows only the second output for the changed/re-rendered component:

exports[`App increments the counter 2`] = `
<div>
  <h1>
    My Counter
  </h1>
  <div>
    <p>
      1
    </p>
  </div>
  <button
    onClick={[Function]}
    type="button"
  >
    Increment
  </button>
  <button
    onClick={[Function]}
    type="button"
  >
    Decrement
  </button>
</div>
`;

Again, that’s the most basic approach for testing a changed/re-rendered component. However, there are two drawbacks for this minimal approach which can be seen in the previous snapshot’s output:

    1. The entire component gets snapshotted again. (Redundancy)
    1. It’s not clear that the snapshot was performed to assert a change regarding a re-rendered component. Rather it’s just a straightforward snapshot again. (Missing Context)

Let’s implement a better version for snapshot tests to assert differences that can happen after re-renderings caused by user interaction or other side-effects. First, install this neat helper library for asserting a snapshot difference:

npm install --save-dev snapshot-diff

Second, setup the helper library by extending your Jest expect method with a new functionality:

import React from 'react';
import renderer from 'react-test-renderer';
import { toMatchDiffSnapshot } from 'snapshot-diff';


expect.extend({ toMatchDiffSnapshot });


import App from './App';

describe('App', () => {
  it('increments the counter', () => {
    ...
  });
});

And third, make use of the new functionality to create a snapshot for the difference between two component renders:

import React from 'react';
import renderer from 'react-test-renderer';
import { toMatchDiffSnapshot } from 'snapshot-diff';

expect.extend({ toMatchDiffSnapshot });

import App from './App';

describe('App', () => {
  it('increments the counter', () => {
    const component = renderer.create(<App />);
    const tree = component.toJSON();
    expect(tree).toMatchSnapshot();

    component.root.findAllByType('button')[0].props.onClick();

    const treeUpdate = component.toJSON();
    expect(tree).toMatchDiffSnapshot(treeUpdate);
  });
});

Now, you get the second output for the re-rendered component in your snapshot output file:

exports[`App increments the counter 2`] = `
"Snapshot Diff:
- First value
+ Second value

@@ -2,11 +2,11 @@
    <h1>
      My Counter
    </h1>
    <div>
      <p>
-       0
+       1
      </p>
    </div>
    <button
      onClick={[Function onClick]}
      type=\\"button\\""
`;

If you compare this snapshot’s output to the previous one, you can see that we got rid of the two mentioned drawbacks. First, we don’t render the whole component again, but only the part that has changes in addition to its surrounding environment. Second, the snapshot test’s output doesn’t look like a rendered component anymore, but like a diff between two outputs shown with the + and - prefixes. Only by looking at the snapshot’s output file, a developer can tell that 1) the snapshot test was caused by a change of the component and 2) that the rendered output has changed from X to Y.

Recommended Reading

Secure Your React and Redux App with JWT Authentication

How to Using WebSockets in React

Made an app, lets you find all your reddit saves (100% open source)

How to Making Animations In React Native

14 Beneficial Tips to Write Cleaner Code in React Apps

What is a Virtual DOM and why does React use it?

#reactjs #javascript

How to Jest Snapshot Test the Difference
17.45 GEEK