Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kolodny/49fd4b6d2bfb422cda7c4f2b7dfeac0a to your computer and use it in GitHub Desktop.
Save kolodny/49fd4b6d2bfb422cda7c4f2b7dfeac0a to your computer and use it in GitHub Desktop.
Visual Regression Testing in Jest

Visual Regression Testing with Jest

This is a walkthrough of how to set up Visual Regression Testing with Jest for an application created with create-react-app.

The following walkthrough uses React as an example, but the approach should work for any modern frontend library! I assume it can be used with Angular, Vue, Cycle.js and more.

This gist walks you through a create-react-app application as an example of how to set up Visual Regression Testing in Jest using libraries I wrote recently which enable this: jsdom-screenshot, jest-transform-css and jest-transform-file.

I will write a more detailed article soon. This gist is for the curious.

You can also check out this repo which is what you'll end up with in case you get confused along the way.

ToC

Set up create-react-app

Create a new create-react-app project called "vrt-cra" (short for visual-regression-testing-create-react-app), or with whatever name you prefer.

npx create-react-app vrt-cra

Open project

cd vrt-cra

Eject project, as we need a more sophisticated setup.

You will be asked whether you really want to eject, confirm it by typing y and pressing Enter.

yarn eject

You can start the application to see what it looks like by running yarn start.

You can also run the tests once to ensure they work properly with yarn test App.test.js. You have to quit them with q after they ran as they start in watch mode automatically.

Set up Visual Regression Testing libraries

Now we need to add some libraries to enable Visual Regression Testing: jsdom-screenshot, jest-transform-css and jest-transform-file:

yarn add jest-image-snapshot jsdom-screenshot jest-transform-file jest-transform-css

Edit package.json to add the Visual Regression Testing libraries to Jest.

// package.json
"jest": {
  // ... more stuff ...

  // add this entry
  "setupTestFrameworkScriptFile": "./src/setupTests.js",

  // this entry already exists, change it from "node" to "jsdom"
  "testEnvironment": "jsdom",

  // this entry already exists, change it
  "transform": {
    "^.+\\.(js|jsx|mjs)$": "<rootDir>/node_modules/babel-jest",
    "^.+\\.css$": "jest-transform-css",
    "^.+\\.svg$": "jest-transform-file",
    "^(?!.*\\.(js|jsx|mjs|css|json|svg)$)": "<rootDir>/config/jest/fileTransform.js"
  },

  // ... more stuff ...
}

Notice that we added "svg" to the list of files not handled by fileTransform.js in the last line, as we're now transforming SVGs through jest-transform-file instead.

Extend the expect with the toMatchImageSnapshot function so that we can compare images.

// src/setupTests.js
import { toMatchImageSnapshot } from "jest-image-snapshot";
expect.extend({ toMatchImageSnapshot });

Adapt the App.test.js test.

// src/App.test.js
import React from "react";
import ReactDOM from "react-dom";
import { generateImage } from "jsdom-screenshot";
import App from "./App";

it("has no visual regressions", async () => {
  // render App into jsdom
  const div = document.createElement("div");
  document.body.appendChild(div);
  ReactDOM.render(<App />, div);

  // prevent spinner from rotating to ensure consistent screenshots
  document
    .getElementsByClassName("App-logo")[0]
    .setAttribute("style", "animation: none");

  // Take screenshot with generateImage()
  const screenshot = await generateImage();
  // and compare it to the previous sceenshot with toMatchImageSnapshot()
  expect(screenshot).toMatchImageSnapshot();

  // clean up for next test
  ReactDOM.unmountComponentAtNode(div);
  document.body.removeChild(div);
});

🎉 This is already the end of the setup for basic Visual Regression Testing!

We can now rerun the tests with yarn test App.test.js.

Notice that you must restart your tests completely (exit the "watch" mode) as we changed the configuration quite a bit.

You will see a screenshot was saved to src/__image_snapshots__. The next time you run your tests, another screenshot will get taken and compared with the one already existing there. If they match, your tests succeed as there were no visual regressions. When they differ, an image showing the differences will be created and your tests will fail. In case the changes were on purpose, the saved image can be updated by pressing u in the tests, as may already know from Jest's snapshotting feature (not to confuse with screenshots).

This is great as it gives you confidence that your layout did not change. You don't need to make any tests for classnames anymore, just take a screenshot instead.

Make some changes to App.js and see how it affects the tests.

A sidenote on performance: Taking a screenshot (the generateImage function) takes one or two seconds, depeding on your system and the size of the sreenshot. So use the feature wisely.

Add react-testing-library

The setup is quite cumbersome for each test as we're manually mounting the div, rendering the application and cleaning up. We can let react-testing-library handle that. If you're not using React, you can use dom-testing-library instead.

Add react-testing-library and jest-dom.

yarn add react-testing-library jest-dom

Enable jest-dom helpers and clean up react-testing-library automatically.

// src/setupTests.js

// add some helpful assertions
import "jest-dom/extend-expect";

// clean up after each test
import "react-testing-library/cleanup-after-each";

// This was here before as we added it earlier. We still need it.
import { toMatchImageSnapshot } from "jest-image-snapshot";

expect.extend({ toMatchImageSnapshot });

And now, we can clean up our test in App.test.js:

// src/App.test.js
import React from "react";
import { generateImage } from "jsdom-screenshot";
import { render } from "react-testing-library";
import App from "./App";

it("has no visual regressions", async () => {
  // render App into jsdom
  render(<App />);

  // prevent spinner from rotating to ensure consistent screenshots
  document
    .getElementsByClassName("App-logo")[0]
    .setAttribute("style", "animation: none");

  // Take screenshot with generateImage()
  const screenshot = await generateImage();
  // and compare it to the previous sceenshot with toMatchImageSnapshot()
  expect(screenshot).toMatchImageSnapshot();
});

More features

Interacting with components before taking the screenshot

It is possible to interact with any component before taking the screenshot. The screenshot will contain whatever the jsdom used in tests contains at that moment. See react-testing-library for more information about that.

PostCSS, CSS Modules, Styled Components, ..

This example showed how to use jest-transform-css with global CSS. The library can also handle CSS modules and Styled Components. See jest-transform-css for setup instructions.

Request interception

In case your components make requests when mounted, you can use Request Interception to respond to requests from the tests. See jsdom-screenshot for more information.

Static File Serving

A tiny webserver gets started by passing generateImage({ serve: ['public'] }) which can serve local assets for the screenshots. See jsdom-screenshot for more information.

Debugging

It is possible to print the markup that the screenshot gets taken of by passing generateImage({ debug: true }). See jsdom-screenshot for more information.

Summary

This setup has shown how to do Visual Regression Testing in Jest by the example of a create-react-app application. We were able to load the component's styles and the SVG file (or any other images). The walkthrough hinted at how we can use

A more advanced setup can be found at visual-regression-testing-example.

Disclaimer

This setup is highly experimental. I tried it with a few different configurations and it worked fine so far, but I'm sure there is more that needs to be fixed. Handle it with care, it is early stage!

Feel free to ask any questions on Twitter: @dferber90!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment