Storybook Snapshot Testing

Automatic Jest Snapshot Testing with Storybook Stories

By Michael Chang

January 12th, 2021

image

My Journey

With my component library maturing, I began focusing on testing. Since these components were the visual building blocks of my applications, I wanted to use Jest’s snapshots to ensure there weren’t unintended visual changes. Soon after I started writing the tests, I realized that there was a feeling of déjà vu — I had coded a bunch of similar stuff with Storybook!

In Storybook, I demo my components’ capabilities and features by creating stories for each component prop so users can visually see and interact with different variations. Rendering components with different props was exactly what I was doing with my tests! Since I had already done all this work showcasing the capabilities of my component library through Storybook, could I leverage it for snapshot testing?

Storyshot

Storybook’s Storyshot Addon does exactly this: it repurposes existing Storybook stories for Jest snapshot testing. It takes these stories and creates a snapshot for each story automatically.

Setup

Setup is incredibly easy. Simply create a test file that can be picked up by Jest and run the Storyshots addon.

// Storybook.test.js

import initStoryshots from '[@storybook/addon-storyshots](http://twitter.com/storybook/addon-storyshots)';

initStoryshots();

Testing

Once configured, Storyshot testing will behave exactly the same as other snapshot tests. Running jest will run the test suite. If the snapshot file(s) don’t exist yet, Jest will create them. If they do, Jest will compare the Storyshot snapshots with the saved snapshots and error if there is any mismatch. Running jest --update-snapshot will regenerate the saved snapshots to resolve any errors.

MDX

Storybook supports markdown (MDX) stories such as the following.

// stories/Button.stories.mdx

import { Meta, Story } from "[@storybook/addon-docs](http://twitter.com/storybook/addon-docs)/blocks";
import { Button } from "./Button";

<Meta title="Example/Button MDX" component={Button} />

# Button
### This is a demo of a Button markdown documentation page.

- This is a primary button:

<Story name="Primary">
  <Button label="Button" primary />
</Story>

Storyshots supports snapshot testing for these type of stories too. But to do so, Jest needs to be able to understand and parse the markdown syntax. The Storybook Docs Addon, which enables markdown stories, provides jest-transform-mdx to accomplish this.

// jest.config.js or "jest" field in package.json

{
  "transform": {
    "^.+\\.mdx?$": "@storybook/addon-docs/jest-transform-mdx",
  }
}

Now, Storyshots can go through all the markdown files and generate snapshots for all the Story blocks.

Required Function Props

One common configuration for Storybook is to automatically inject a Storybook Action Logger function for all props that match the ^on[A-Z].* regex. This function will log every invocation to the Storybook actions tab so users can understand when and how it is triggered.

// .storybook/preview.js

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
}

This injection unfortunately does not happen for Storyshots. This isn’t a problem for optional function props but is a problem for required ones (through PropTypes.func.isRequired). The test will fail because the PropType check fails.

To get around this, we’ll have to update the stories to manually add values.

// stories/Header.stories.js

export default {
  title: 'Example/Header',
  component: Header,
  args: {
    ...(process.env.NODE_ENV === 'test'
      ? {
          onLogin: () => {},
          onLogout: () => {},
          onCreateAccount: () => {},
        }
      : {}),
  },
};

By checking process.env.NODE_ENV, we can conditionally add the values only if the story is being run in a test so both the snapshot test succeeds in the test environment and the action logger works in the Storybook UI.

React Portals

If any of your components use React Portals, you will need to mock the createPortal function to essentially disable the portal effect. Storyshots snapshots the content under the root div (

). If you portal the content to somewhere outside of this (such as document.body), Storyshot will not be able to find it and your snapshot will be incomplete. This snippet of code will mock the createPortal function to not perform the portal effect and return the node directly.

// Storybook.test.js

jest.mock('react-dom', () => {
  const original = jest.requireActual('react-dom');
  return {
    ...original,
    createPortal: node => node,
  };
});

initStoryshots();

Disable / Enable

Storyshot, unfortunately, can’t be used for all components. For example, Modal and Tooltip components are only visible after some user interaction. A snapshot test of these components’ stories would result in a snapshot of the interactive element (ie a button) and not the Modal and Tooltip component, and, therefore, be a pointless test. To prevent these, Storyshots can be disabled or enabled on a per component or story basis through parameters.storyshots.disable.

// stories/Page.stories.js

export default {
  title: 'Example/Page',
  component: Page,
  parameters: {
    storyshots: { disable: true },
  },
};

const Template = args => <Page {...args} />;

export const LoggedIn = Template.bind({});
LoggedIn.args = {
  user: {},
};

export const LoggedOut = Template.bind({});
LoggedOut.parameters = {
  storyshots: { disable: false },
};

Note that even if Storyshots is disabled for the component, you can still reenable it for an individual component story.

Additional Configuration Options

The initStoryshots function can also take additional configuration options to customize the Storyshot experience. Here are a few common configurations:

**snapshotSerializer **allows you to pass an array of custom serializers. This is useful if you need to use custom serializers because of CSS in JS frameworks such as Styled Components or Emotion.

import initStoryshots from '[@storybook/addon-storyshots](http://twitter.com/storybook/addon-storyshots)';
import { styleSheetSerializer } from 'jest-styled-components/serializer';

initStoryshots({
  snapshotSerializers: [styleSheetSerializer],
});

test allows you to run a custom test function for each story. While not particularly useful for typical configurations, it can be used with Storyshot’s multiSnapshotWithOptions to generate separate snapshot files per component instead of one gigantic file all components.

import initStoryshots, { multiSnapshotWithOptions } from '[@storybook/addon-storyshots](http://twitter.com/storybook/addon-storyshots)';

initStoryshots({
  test: multiSnapshotWithOptions(),
});

stories2snapsConverter allows you to customize snapshot and story filenames and locations. For example:

import initStoryshots, { Stories2SnapsConverter } from '[@storybook/addon-storyshots](http://twitter.com/storybook/addon-storyshots)';

initStoryshots({
  stories2snapsConverter: new Stories2SnapsConverter({
    snapshotsDirName: '__snapshots__',
    snapshotExtension: '.js.snap',
    storiesExtensions: ['.js', '.mdx'],
  }),
});

See Storyshot documentation for a full list of configuration options.

Final Thoughts

Storyshots leverages all the work you have done to create your Storybook and provides automatic visual testing. My component library had over 50 component stories each with up to a dozen stories. All told, I generated over 300 tests in a matter of 5 minutes. And just like that, my component library was fully tested.

Resources



Continue Learning