PHP
Article
By Christopher Pitt

What Is Snapshot Testing, and Is It Viable in PHP?

By Christopher Pitt
Help us help you! You'll get a... FREE 6-Month Subscription to SitePoint Premium Plus you'll go in the draw to WIN a new Macbook SitePoint 2017 Survey Yes, let's Do this It only takes 5 min

This article was peer reviewed by Matt Trask, Paul M. Jones, and Yazid Hanifi. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!


A vector image of a polaroid glued to a transparent background

Ah-ha moments are beautiful and rare in programming. Every so often, we’re fortunate enough to discover some trick or facet of a system that forever changes how we think of it.

For me, that’s what snapshot testing is.

You probably write a lot of PHP code, but today I want to talk about something I learned in JavaScript. We’ll learn about what snapshot testing is and then see how it can help us write better PHP applications.

Building Interfaces

Let’s talk about React. Not the kick-ass async PHP project, but the kick-ass JavaScript project. It’s an interface-generation tool in which we define what our interface markup should look like as discrete parts:

function Tweet(props) {
  return (
    <div className="tweet">
      <img src={props.user.avatar} />
      <div className="text">
        <div className="handle">{props.user.handle}</div>
        <div className="content">{props.content}</div>
      </div>
    </div>
  )
}

function Tweets(props) {
  return (
    <div className="tweets">
      {props.tweets.map((tweet, i) => {
        return (
          <Tweet {...tweet} key={i} />
        )
      })}
    </div>
  )
}

This doesn’t look like vanilla Javascript, but rather an unholy mix of HTML and Javascript. It’s possible to create React components using regular Javascript syntax:

function Tweet(props) {
  return React.createElement(
    "div",
    { className: "tweet" },
    React.createElement("img", { src: props.user.avatar }),
    React.createElement(
      "div",
      { className: "text" },
      React.createElement(
        "div",
        { className: "handle" },
        props.user.handle
      ),
      React.createElement(
        "div",
        { className: "content" },
        props.content
      )
    )
  );
}

To make this code, I pasted the Tweet function (above) into the Babel REPL. That’s what all React code is reduced to (minus the occasional optimization) before being executed by a browser.

Before I talk about why this is cool, I want to address a couple of issues…

“Why Are You Mixing HTML and Javascript?!”

We’ve spent a lot of time teaching and learning that markup shouldn’t be mixed with logic. It’s usually couched in the phrase “Separation of Concerns”. Thing is, splitting HTML and the Javascript which makes and manipulates that HTML is largely without value.

Splitting that markup and Javascript isn’t so much separation of concerns as it is separation of technologies. Pete Hunt talks about this in more depth in this video.

“This Syntax Is Very Strange”

That may be, but it is entirely possible to reproduce in PHP and works out the box in Hack:

class :custom:Tweet extends :x:element {
  attribute User user;
  attribute string content;

  protected function render() {
    return (
      <div class="tweet">
        <img src={$this->:user->avatar} />
        <div class="text">
          <div class="handle">{$this->:user->handle}</div>
          <div class="content">{$this->:content}</div>
        </div>
      </div>
    );
  }
}

I don’t want to in detail about this wild syntax except to say that this syntax is already possible. Unfortunately, it appears the official XHP module only supports HHVM and old versions of PHP…

Testing Interfaces

There are many testing approaches – some more effective than others. An effective way to test interface code is by faking (or making) a web request and inspecting the output for the presence and content of specific elements.

Perhaps you’ve heard of things like Selenium and Behat? I don’t want to dwell too much on them. Let’s just say that Selenium is a tool we can use to pretend to be a browser, and Behat is a business-friendly language for scripting such pretense.

Unfortunately, a lot of browser-based testing can be brittle. It’s tied to the exact structure of markup, and not necessarily related to the logic that generates the markup.

Snapshot testing is a different approach to doing the same thing. React encourages thinking about the whole interface in terms of the smallest pieces it can be broken down into. Instead of building the whole shopping cart, it encourages breaking things up into discrete parts; like:

  • each product
  • the list of products
  • the shipping details
  • the progress indicator

In building each of these pieces, we define what the markup and styles should be, given any initial information. We define this by creating a render method:

class Tweets extends React.Component {
  render() {
    return (
      <div className="tweets">
        {props.tweets.map((tweet, i) => {
          return (
            <Tweet {...tweet} key={i} />
          )
        })}
      </div>
    )
  }
}

…or by defining a plain function which will return a string or React.Component. The previous examples demonstrated the functional approach.

This is an interesting way of thinking about an interface. We write render as though it’ll only be called once, but React is constantly reconciling changes to the initial information, and the component’s own internal data.

And it’s this way of thinking that leads to the simplest way to test React components: Snapshot Testing. Think about it for a minute…

We build React components to render themselves, given any initial information. We can work through all possible inputs in our head. We can even define strict validation for what initial information (or properties) we allow into our components.

So, if we can work through those scenarios while designing our component: then we can work through them in a test. Let’s create a new component:

const Badge = function(props) {
  const styles = {
    "borderColor": props.borderColor
  }

  if (props.type === "text") {
    return (
      <div style={styles}>{props.text}</div>
    )
  }

  return (
    <img style={styles} src={props.src} alt={props.text} />
  )
}

This is our Badge component. It can be of two types: text and image. It can also have a border color. We can validate and create defaults for these properties:

const requiredIf = function(field, value, error) {

  // custom validators expect this signature
  return function(props, propName, componentName) {

    // if props.type === "image" and props.src is not set
    if (props[field] === value && !props[propName]) {

      return new Error(error)
    }
  }
}

Badge.propTypes = {
  "borderColor": React.PropTypes.string,
  "type": React.PropTypes.oneOf(["text", "image"]),
  "src": requiredIf("type", "image", "src required for image")
}

Badge.defaultProps = {
  "borderColor": "#000",
  "type": "text"
}

So, what are the ways in which this component can be used? There are a few variations:

  • Without specifying a borderColor or type, a text badge is rendered and it has a black border.
  • Changing type to image requires that src is also set, and renders an image badge.
  • Changing borderColor to anything changes the border color of text and image badges alike.

It’s beginning to sound like a test. What if we called the rendered component with a well-defined initial set of data, a snapshot? We could describe these scenarios with some JavaScript:

import React from "react"
import Tweets from "./tweets"

import renderer from "react-test-renderer"

test("tweets are rendered correctly", function() {
  const defaultBadge = renderer.create(
    <Tweets>...</Tweets>
  )

  expect(component.toJSON()).toMatchSnapshot()

  const imageBadge = renderer.create(
    <Tweets type="image" src="path/to/image">...</Tweets>
  )

  expect(component.toJSON()).toMatchSnapshot()

  const borderedBadge = renderer.create(
    <Tweets borderColor="red">...</Tweets>
  )

  expect(component.toJSON()).toMatchSnapshot()
})

We need to get Jest set up, before we can run this code.

With each modification to the initial properties, a new set of markup may be rendered. We should check that each variation matches a snapshot we know to be accurate.

With this, we can see that snapshot testing is a means by which we can predict the output of a given function (or the internal state of a given object), and compare future output to it. With a well-defined blueprint of the data we expect our code to give, we can easily tell when the output is unexpected or incorrect.

This has an older name: Characterization Testing. We write characterization (or snapshot) tests by establishing well-formed, expected output. Subsequent tests compare the output of our code against the baseline, using the same inputs.

Now, let’s think of scenarios where it could be useful to us…

--ADVERTISEMENT--

Snapshot Testing in PHP

We’re going to look at a few use-cases for snapshot testing. Before we do, let me introduce you to a PHPUnit snapshot testing plugin: https://github.com/spatie/phpunit-snapshot-assertions

You can install it via:

composer require --dev spatie/phpunit-snapshot-assertions

It provides a number of helpers in the form of:

use PHPUnit\Framework\TestCase;
use Spatie\Snapshots\MatchesSnapshots;

class ExampleTest extends TestCase
{
  use MatchesSnapshots;

  public function testExamples()
  {
    $this->assertMatchesSnapshot(...);
    $this->assertMatchesXmlSnapshot(...);
    $this->assertMatchesJsonSnapshot(...);
  }
}

These methods create the same kinds of snapshot files as Jest does. In general, we only need to use the first one, but the second and third generate nicer diffs for their respective content types.

Templates

The first, and sort of obvious, use for this is in testing templates. The smaller our templates, the easier it should be to write snapshot tests for them. This is particularly useful when using template engines which accept initial properties:

<div class="tweet">
  <img src={{ $user->avatar }} />
  <div class="text">
    <div class="handle">{{ $user->handle }}</div>
    <div class="content">{{ $content }}</div>
  </div>
</div>
Route::get("/first-tweet", function () {
  return view(
    "tweet", Tweet::first()->toArray()
  );
});

It’s clear what the template should produce, given repeatable initial information. We could even mock the initial data, and assert the output:

namespace Tests\Unit;

use Spatie\Snapshots\MatchesSnapshots;
use Tests\TestCase;

class ExampleTest extends TestCase
{
  use MatchesSnapshots;

  public function testTweetsRenderCorrectly()
  {
    $user = new User();
    $user->avatar = "path/to/image";
    $user->handle = "assertchris";

    $tweet = new Tweet();
    $tweet->user = $user;
    $tweet->content = "Beep boop!";

    $rendered = view(
      "tweet", $tweet
    );

    $this->assertMatchesSnapshot($rendered);
  }
}

I’m showing examples from a Laravel application (buy our introductory course here!). Laravel uses PHPUnit under the hood, so the examples would work outside of Laravel too, with enough modification. Laravel is a useful starting-point because it ships with a template engine, routing, and an ORM.

Event Sourcing

Event Sourcing architectures are particularly well-suited to snapshot testing. There’s a lot to them, so feel free to read up on them!

The basic idea behind Event Sourcing is that the database is write-only. Nothing is ever deleted, but every meaningful action leads to an event record. In contrast with most CRUD applications which freely create and delete the latest state of records, Event Source applications add records which represent all the actions.

Snapshot testing works with or without Event Sourcing, but testing an event stream is tedious without it. Imagine being able to assert the latest state of a record, but also effortlessly assert every step that got the record to that state.

// continuing from sitepoint.com/event-sourcing-in-a-pinch...

$events = [
  new ProductInvented(...),
  new ProductPriced(...),
  new OutletOpened(...),
  new OutletStocked(...),
];

$this->assertMatchesSnapshot($events);

$projected = project($connection, $events);

$this->assertMatchesSnapshot($projected);

Even before these assertions, it’s useful to simulate browser activity (or whatever input medium this application enables) to assert whether interface interactions generate a snapshot that matches the snapshot of generated events.

Queued Tasks

Asynchronous systems are particularly troublesome to test. At worst we ignore these parts of our application. At best, we struggle through the process of mocking the queue provider for every deferred interaction with the system. What if we could test all queued tasks at once, for our entire test suite?

php artisan queue:table
php artisan migrate

This creates a database table, for the database queue driver.

return [
  "default" => env("QUEUE_DRIVER", "database"),
  // ...remaining configuration
];

This is from config/queue.php.

namespace Tests\Unit;

use Spatie\Snapshots\MatchesSnapshots;
use Tests\TestCase;

class ExampleTest extends TestCase
{
  use MatchesSnapshots;

  public function testQueueSomething()
  {
    // ...do something that leads to dispatch(new Job);
  }

  public function testQueueSomethingAgain()
  {
    // ...do something that leads to dispatch(new AnotherJob);
  }

  public function testQueue()
  {
    $table = config("queue.connections.database.table");
    $jobs = app("db")->select("select * from {$table}");

    $this->assertMatchesSnapshot($jobs);
  }
}

Once the jobs are deleted, they’ll no longer be returned by the select statement. You’ll have to make sure no queue listener/daemon is running for the same database table while the tests are running.

Brittle Tests

You might think that snapshot tests are brittle. Just as brittle as poorly written interface tests. Only slightly more brittle than well-written interface tests. And you’d be right.

The question we should be asking is why brittle tests are a problem. An application with some useful tests is in a better place than an application with no tests at all. But when the tests are brittle then any refactoring will break them. Even inconsequential or cosmetic changes are likely to break the tests. Changing the class of a div from “some-style” to “some–style”? If you’re using that in a CSS selector then it’s likely to break the tests.

The problem with brittle tests is that rewriting tests takes time. And we don’t want to rewrite tests all the time, especially when tiny changes have made them break prematurely.

But we’re not actually writing any tests with Snapshot Testing. Not really. Sure, we can write unit tests and integration tests to compliment our Snapshot tests. But when we write these tests we’re just taking something serializable and comparing it to what it was serialized as in the first place. And when they break, after we’re sure nothing has actually broken, we can just delete the snapshot and try again.

In a sense, snapshot testing makes non-TDD testing more interactive. It’s OK that these tests are brittle because they’re easy to regenerate, and still useful in many ways.

Summary

I had a wonderful ah-ha moment when I first learned about Snapshot Testing. Since then, most of the front-end tests I write are snapshot tests. It’s been interesting considering use-cases for it in PHP. I hope you’re as excited to dig into it as I am!

Can you think of more places where this would be useful? Tell us about them in the comments!

Thanks, Paul Jones, for pointing me towards Characterization Testing.

Login or Create Account to Comment
Login Create Account
Recommended
Sponsors
Get the most important and interesting stories in tech. Straight to your inbox, daily.Is it good?