Say Cheese: Snapshots and Visual Testing

Mateusz Burzyński in Programming, Tutorials, on November 01, 2017, Comments

Visual Testing

Creating reusable, well-tested, change-resistant UI is hard. It gets even more complicated if the slightest change can affect over 21,000 of your customers and millions of their visitors. Believe us, we’re speaking from experience: been there, done that.

Therefore we’ve decided to build our very own React UI kit.

It will make working with LiveChat Visitor JS SDK a lot easier, but of course, we plan to use it directly in our chat widget, too.

Creating such library is not a piece of cake. We need to carefully componentize our UI, establish each component’s API and — this is the hardest part — make it customizable as much as possible without making the whole system too fragile.

The (im)perfect testing

When it comes to producing change-resistant software, you may think of tests. But how many times have your unit tests failed and allowed a bug to slip to production? Not one? Congratulations, you are unbelievably lucky or unbelievably skillful! Don’t get us started on UI tests. The majority is incredibly brittle, tightly coupled with CSS selectors and the HTML structure, oftentimes not even tested in the browser environment.

That got us thinking: can’t we do better than this to protect our customers and ourselves?

Welcome to snapshot and visual testing

Visual testing got really popular recently thanks to Jest test runner. There is plenty of great resources on this topic, such as this recording. But to sum it up and give you a gist:

  1. When executing your particular test for the very first time, a test runner dumps the tested object to some serializable format and saves it on your hard drive. The object can be anything from a number to a React component.
  2. During the subsequent runs it repeats the process and then it compares the saved snapshot with the new one.
  3. If there are any differences, the test is failed.
  4. If the changes were intentional, you can just accept them with a single keystroke. If not, you can go back to the code to fix it.

Visual Testing

Such tests can provide high coverage fast because they are very easy and quick to write. If used correctly, they can also give you nice visual reports outlining the differences across the files. The obvious downside is that updating snapshots is somewhat error-prone because you need a human to do it. On the other hand, though, this applies to other types of tests as well.

Each test is only as good as its designer.

Visual testing is pretty much what it sounds to be. In a word, you take the visual output of your code (a rendered page) and check it for errors. Thus, it seems like a perfect match for snapshot testing. Luckily for us, American Express have done a part of the heavy lifting already with their **jest-image-snapshot **matcher.

From theory to practice

Let’s connect the dots, then. We still need to figure out how to supply image data to our tests. For documenting our component library we selected React Styleguidist because we liked the way we can create our own examples and how users can interact with them.

The tool serves us a webpage with a UI kit documentation which we scrap for metadata with another great tool, Puppeteer. It provides us with a rich JS API for controlling the Chrome browser (headless by default). You can see it in action here.

With the extracted metadata it is easy to generate most of our basic test cases automatically in Jest:

import path from 'path'
import fs from 'fs'
import puppeteer from 'puppeteer'

const browserPromise = puppeteer.launch()
const examples = require('./path/to/metadata.json')

describe("Examples' image snapshots", () => {
  for (const example of examples) {
    test(`[${example.component}]: ${example.title}`, async () => {
      const browser = await browserPromise
      const page = await browser.newPage()

      await page.goto(example.url, { waitUntil: 'load' })

      const handle = await page.$(example.selector)
      const screenshot = await handle.screenshot({ omitBackground: true })

      expect(screenshot).toMatchImageSnapshot({
        customSnapshotIdentifier: `${example.component}-${example.title}`,
      })
    })
  }

  afterAll(async () => (await browserPromise).close())
})

And there you are!

What’s next?

Thanks to visual tests, we can make releases with greater confidence. We can also run tests with our customers’ configurations to be sure that we won’t break anyone’s site.

Moreover, we can bring our designers closer to the release process. All it takes is automating the posting of “before” and “after” screenshots by creating a bot to make pull requests on GitHub.

If you have any questions about the tools that we use or about visual testing itself, ask away!