4 Steps to Visual Regression Testing

Lynn Jepsen
Feb 17, 2019 · 9 min read

Update: I wrote a new and improved version of this blog post here: Be Fearless with Screenshot Testing and routine-design

Do you review pull requests (PR) with HTML/JSX/React and CSS/Sass code? Do you find yourself relying on the PR author to test that the HTML/CSS actually works? Is it stressful when the PR author makes a mistake?

What if you had a safety net, so it was impossible to submit HTML/CSS code that does not work?

Photo by Alberto Bigoni on Unsplash

You can set up visual regression testing of your HTML/CSS in four easy steps. First, I’ll show you how to capture screenshot images on a Docker image. This makes it easier to compare screenshots from a local developer environment to a Continuous Integration (CI) environment. Next, I’ll show you how to write Mocha tests, which verify HTML/CSS code matches your captured screenshot images. Next, I’ll show you how to set up TravisCI, which is the safety net for all PRs. And finally, I’ll show you how to review changes to screenshots, so your Web app can keep evolving.

Capture on Docker

Docker is a way to run containers, built from images. I’ve created a routine-design Docker image, which packages up code needed to capture a screenshot with routine-design. This containerized software will always run the same, regardless of the infrastructure. Screenshot images captured on a local Mac will be the same as screenshot images captured on a remote Linux environment, like TravisCI.

diagram showing Docker’s containerized applications

But first, Read and follow the instructions from my two previous blog posts:

By the end you will have an automated way of taking a screenshot of individual components.

Next, install Docker Desktop on you local developer environment. You will be compiling Sass code on this docker image, so build a compatible binding for node-sass. There’s a handy routine-design command for that. Update your package.json with this new postinstall command

"scripts": {   "postinstall": "docker pull lynnmercier/routine-design && routine-design docker build-node-sass",  // other scripts like  // "start": "react-scripts start"},

Run npm i to pull the docker image and build node-sass. That one-line command is all you need to use routine-design in both your developer and CI environments.

Routine-design provides APIs for managing routine-design docker containers. It also provides an API, ComponentTree, to easily scrape over test/render directory. So create a new capture.js file and import routine-design

const RoutineDesign = require('routine-design');class Capturer {

async run() {
const routineDesign = new RoutineDesign(); //TODO fill me in with code

}
}new Capturer().run();

Using RoutineDesignContainer, start a Web server that renders individual components in the test/render directory. You’re basically running npm run render on the docker container. This command runs in detached mode, meaning you can send other commands to RoutineDesignContainer while the Web server is running. Next, for each directory, tell RoutineDesignContainer to capture a screenshot. You can do this with a single routine-design CLI command.

const routineDesignContainer = routineDesign.getLocalStorage().createRoutineDesignContainer();await routineDesignContainer.start();await routineDesignContainer.run('routine-design render ./test/render', true);const directories = routineDesign.createRoutineDesignTree('./test/render').getComponentTree().getDirectories();for (let entry of directories) {

const componentDirectoryId = entry[0];

await routineDesignContainer.run('routine-design directory capture project-id my-app-screenshots ./test/render --component-directory='+componentDirectoryId);
}await routineDesignContainer.cleanup();

Update your package.json with a new capture command

"scripts": {  "capture": "node ./capture.js",  // other scripts},

Before you can run capture.js, you need to set up your Google Cloud authentication environment variables. The ROUTINE_DESIGN_GOOGLE_CREDS variable will work for both your developer and CI environments. Assuming you already have GOOGLE_APPLICATION_CREDENTIALS set, just run this command

export ROUTINE_DESIGN_GOOGLE_CREDS=$(cat $GOOGLE_APPLICATION_CREDENTIALS)

Now run npm run capture. This process can take 45 seconds, so be patient. Once it is finished, run git status to see which files have changed. You should see modifications to test/render/image.json and test/render/logo/image.json. The docker container actually mounts your local directory, so when it runs routine-design directory capture any changes to image.json are also modifications to your local directory.

Open the image URLs in the image.json files to see the new versions of the screenshot images. Congratulations, you’ve captured containerized versions of screenshot images! Now with a one-line command you can capture screenshot images of all components in test/render.

If you have any problems with routine-design, be sure to file bugs in the GitHub repository.

Testing Screenshots

You can use the Mocha test framework to write individual tests for each component directory. Install mocha as a developer dependency

npm install --save-dev mocha

Create a new test/render.test.js file, where you will write your Mocha tests.

const RoutineDesign = require('routine-design');const assert = require('assert');describe('render', function() {

const routineDesign = new RoutineDesign();

//TODO fill me in with code
});

Mocha lets you run specify code to run before all tests have run. Tell Mocha that you want create a RoutineDesignContainer. Also tell RoutineDesignContainer to start a Web server that renders individual components in the test/render directory. This way, the Web server will already be running on the docker container before you take any screenshots.

let routineDesignContainer;before(async function() {  const localStorage = routineDesign.getLocalStorage();  routineDesignContainer = await localStorage.createRoutineDesignContainer();  await routineDesignContainer.start();  await routineDesignContainer.run('routine-design render ./test/render', true);});

Mocha also lets you specify code to run after all tests have run, so make sure you clean up the docker container.

after(async function() {

await routineDesignContainer.cleanup();
});

Use the ComponentTree API to easily scrape over test/render directory. For each directory, tell RoutineDesignContainer to pixel validate the directory. This is a routine-design CLI command for diffing local React/Sass code against screenshots captured in image.json. The CLI command returns a JSON object, with allPass and debugId. AllPass is true when all the local screenshots match their image in image.json. DebugId represents a Google Cloud directory with debug images. Assert that allPass, and include an error message with a URL to the Google Cloud directory.

const directories = routineDesign.createRoutineDesignTree('./test/render').getComponentTree().getDirectories();for (let entry of directories) {

const componentDirectoryId = entry[0];

it(componentDirectoryId, async function() {

const dockerCommand = 'routine-design directory pixel-validate project-id my-app-screenshots ./test/render --component-directory='+componentDirectoryId;

const jsonStr = await routineDesignContainer.run(dockerCommand);

const json = JSON.parse(jsonStr);

let gcpUrl = "https://console.cloud.google.com/storage/browser/my-app-screenshots/";

if (componentDirectoryId != '') {

gcpUrl += componentDirectoryId+"/";

}

gcpUrl += json.debugId+"/?project=project-id";

assert(json.allPass, gcpUrl);

});
}

Update your package.json with a new test command

"scripts": {  "test": "mocha --timeout 60000 test/render.test.js",  // other scripts},

Now run npm test. You should see the Mocha logging to the console that all your tests passed. Congratulations! You’ve just written a test suite for asserting local React/Sass code matches the images captured in image.json! Now with a one-line command you can test screenshot images of all components in test/render.

Continuous Integration

Take a moment to push your code to a GitHub repository. Next you’re going to be working with TravisCI, which is a Continuous Integration service that easily syncs with GitHub. TravisCI can run docker. Sign up for TravisCI (it is free). Go to https://travis-ci.org/account/repositories and make sure your GitHub repository is enabled in TravisCI.

Go to your repository in Travis CI. Click More options > Settings. Add a ROUTINE_DESIGN_GOOGLE_CREDS Environment Variable. You can use this command echo $ROUTINE_DESIGN_GOOGLE_CREDS to print out the value. Copy that value into the TravisCI form, quoting it with single quotes.

TraviCI Settings page, Environment Variables section

Return to your developer environment and make sure you have the master branch checked out. Do a git pull to pull the latest code. Create a new branch called infra/travis-ci

git checkout -b infra/travis-ci

You only need to create a single file to set up TravisCI. Create a new .travis.yml file that specifies your project is a NodeJs project. It also enable the docker service

language: node_js
node_js:
- "11"
services:
- docker

git add . then

git commit -m “infra: set up TravisCI”git push --set-upstream origin infra/travis-ci

This will push a new Git branch to GitHub. From GitHub, you should see a notification to create a new PR from the infra/travis-ci branch. Create that PR. From TravisCI, you should see a build kicked off for the infra/travis-ci commit. Follow along in the logs to see TravisCI run the same npm install and npm test commands you ran locally. You should see the Mocha tests successfully pass, along with the TravisCI job.

In GitHub, navigate to the Branches page, under Settings, for your repository. Add a branch protection rule for the master branch. The rule should require that the branch is up to date and status checks pass. Check the continuous-integration/travis-ci status check.

GitHub branch settings page will status checks enabled

Back in the PR you created, you should see that all checks have passed!

GitHub showing all checks have passed

Now you can confidently “Squash and merge” this PR into the master branch. Now all future PRs will make sure npm test passes, before giving the PR reviewer the green light.

Lets test that CI can actually fail. Return to your developer environment and checkout the latest version of master. Create a new branch called experimental/black-logo

git checkout -b experimental/black-logo

Open src/logo/logo.svg and change the #61DAFB color to #000000. If you run npm start you will see a black logo. If you run npm run render and open localhost:8080/#/logo you will also see a black logo.

git add . and then

git commit -m “experimental: black logo”git push --set-upstream origin experimental/black-logo

Again, from GitHub, you should see a notification to create a new PR from the experimental/black-logo branch. Create that PR. Inside the PR you will find a notification of the running TravisCI job. Click on “Details”

TravisCI pending job notification

Follow along in the logs of that job. You should see the tests fail. The tests fail because the local React/Sass code does not match the images stored in image.json.

Mocha logs error messages for every failed test. The error messages should contain a URL to a Google Cloud directory.

Console log of Mocha test

Open the Google Cloud URL. You should see subdirectories for each screenshot image that failed within the directory. Each subdirectory has a new.png and a diff.png. New.png is the screenshot of the React/Sass code in the PR. Diff.png is the diff between new.png and the image saved in image.json. Congratulations! You’ve collected debug information from a failed CI job. There’s one convenient Google Cloud URL for each failed test.

If you return back to the GitHub PR, you should see the status checks fail. So more congratulations! You’ve given your PR reviewer a safety net against visual regressions.

GitHub showing all checks have failed

Screenshot Review

Lets say we actually want to merge this black logo PR. Return to your developer environment, on the experimental/black-logo branch. Run npm run capture. Remember, this process can take 45 seconds, so be patient.

Once it is finished, run git status to see which files have changed. You should see modifications to test/render/image.json and test/render/logo/image.json. Remember you can open the image URLs in the image.json files to see the new versions of the screenshot images. You should see images with black logos.

git add . and then

git commit -m “capture screenshot images”

Then git push. From GitHub PR you should see a link to a new running TravisCI job. Follow along in the logs of that job. You should see the tests pass!

If you return back to the GitHub PR, you should see the status checks pass. Before you merge this PR, lets review it. Click on the “Files changed” tab. This tab shows diffs between the files on the master branch, and the files on the experimental/black-logo branch. You should see the diffs for both test/render/image.json and test/render/logo/image.json. Image.json has URLs you can easily open to look at the differences between master and experimental/black-logo.

GitHub showing diff in image.json

Look at you! You’re reviewing visual diffs before approving a PR. If the images on experimental/black-logo branch look good compared to the images on the master branch, then merge the PR!

Lynn Jepsen

Written by

Software Engineer. MIT alum. Expert at JavaScript, CSS and HTML. On a mission to align code and design.

Frontend Weekly

It's really hard to keep up with all the front-end development news out there. Let us help you. We hand-pick interesting articles related to front-end development. You can also subscribe to our weekly newsletter at http://frontendweekly.co

Lynn Jepsen

Written by

Software Engineer. MIT alum. Expert at JavaScript, CSS and HTML. On a mission to align code and design.

Frontend Weekly

It's really hard to keep up with all the front-end development news out there. Let us help you. We hand-pick interesting articles related to front-end development. You can also subscribe to our weekly newsletter at http://frontendweekly.co

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store