Resilient Snapshot Testing with Material-UI and React Testing Library

This article was originally published on the DataStax Tech Blog

Using snapshot testing with a library that uses the popular Material-UI component library can create a surprising amount of unnecessary snapshot updates and effectively remove snapshot testing as a valuable tool in your testing toolbox. This article will examine this problem and find a solution to cut down on this extra snapshot noise.

Note: This writeup assumes that you are using Jest, Testing Library (React), and Material-UI in your project.

The Problem

Snapshot tests become much less useful when paired with MaterialUI’s CSS-in-JS solution. At runtime, MaterialUI’s StylesProvider guarantees globally unique class names for your app by adding incremental IDs to the end of the generated class names. This class generation method leads to frivolous snapshot updates like the example below:

<div>
   <div
- class=”makeStyles-wrapper-1"
+ class=”makeStyles-wrapper-2"
   >

Snapshot diffs like this one increase the signal-to-noise ratio of snapshot changes and water down their usefulness. Developers cannot hunt for the meaningful differences in snapshot tests, leading to an increase in bugs introduced to the system.

Solution: Cut down on the className noise

Fortunately, we can modify the behavior of Material-UI’s CSS-in-JS solution and reduce the noise in our snapshots by:

  1. Modifying Material-UI’s class generation function to drop the unique identifiers at the end of class names
  2. Creating a custom render function for React Testing Library using the modified generator function
  3. Using our custom render function in place of the base render function for all of our tests
  4. Updating all existing snapshots to drop the generated class noise.Modifying the class generator

Modifying the Class Generator

Material-UI uses a StylesProvider component to manage the style-related context in our application. This component has a generateClassName prop that allows us to pass in a custom function telling the provider how to construct new class names when needed. We can wire up a function that drops the unique ID that affects our snapshots:

const snapshotFriendlyClassNameGenerator = (rule, styleSheet) =>
  `${styleSheet.options.classNamePrefix}-${rule.key}`

We want to keep our snapshotFriendlyClassNameGenerator as close to our running app as possible, so we retain any prefix or rule key that might be present. This leads to class names like makeStyles-wrapper but without any numbered identifier as a suffix. We can now pair our custom generator function with an instance of StylesProvider:

const SnapshotFriendlyStylesProvider = ({ children }) => (
   <StylesProvider generateClassName
      {snapshotFriendlyClassNameGenerator}>
      {children}
   </StylesProvider>
);

Create a custom render function

In order to introduce our new SnapshotFriendlyStylesProvider into all of our tests, we need to write a custom React Testing Library render function like so:

const customRender = (ui, options) =>
   render(ui, {
      wrapper: SnapshotFriendlyStylesProvider,
      …options,
   });

The subject of custom render functions is not new. The official docs have a great write-up about why you might need one and how to implement one. In a nutshell, we are just wrapping a regular render call in our new SnapshotFriendlyStylesProvider to remove additional class name noise!

Using our custom render function

To see the payoff we want, we need to use our new customRender function instead of the render function provided by React Testing Library. Next, we need to create a testUtils.js file and re-export the rest of React testing library.

export * from “@testing-library/react”;
// Override our render with the snapshot-friendly render.
export { customRender as render };

A final testUtils.js file with all of the previous steps may look like this:

import { render } from “@testing-library/react”;
import { StylesProvider } from “@material-ui/core”;

const snapshotFriendlyClassNameGenerator = (rule, styleSheet) =>
   `${styleSheet.options.classNamePrefix}-${rule.key}`;
const SnapshotFriendlyStylesProvider = ({ children }) => (
   <StylesProvider generateClassName{snapshotFriendlyClassNameGenerator}>
      {children}
   </StylesProvider>
);

const customRender = (ui, options) =>
   render(ui, {
      wrapper: SnapshotFriendlyStylesProvider,
      ...options,
   });

export * from "@testing-library/react";
// Override our render with the snapshot-friendly render.
export { customRender as render };

Finish the job

To complete the change and bring some more resiliency to your snapshots, we need to perform the final step of utilizing our customRender function instead of the out-of-the-box render function provided by React Testing Library and re-generate all of our snapshots. Hence, future changes to our tests generate relevant, slimmed-down snapshot diffs.

To use our new render function, we can update all of our tests as follows (assuming testUtils.js and our target test is in the same directory):

- import { render } from ‘@testing-library/react’;
+ import { render } from ‘./testUtils’;

Finally, we can update all of our test snapshots.

Warning: Make sure you are making this change in isolation; you do not want to accidentally overlook a separate issue that might get buried in the snapshot diffs generated by this step!

# using jest directly
$ jest — updateSnapshot
# create-react-app/react-scripts example
$ npm test — — updateSnapshot — watchAll=false

After this point, all future snapshot tests should not have frivolous style-only diffs for your Material-UI components. Huzzah!

Wrapping Up

By reducing the noise generated by Material-UI’s class names, we can regain the use of snapshot tests in our codebase. We also now know how to construct custom render methods for our tests, allowing us to cut down on boilerplate code in our tests. Finally, we also now have an excellent foundation for future reusable test utilities that we can use to make our tests easier to write and clearer to read.

References