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

Share this article

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

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.

Key Takeaways

  • Snapshot testing captures the output or state of an application and compares it to a previously stored ‘snapshot’, automating the detection of discrepancies in complex data structures or UIs.
  • Unlike traditional PHP testing methods that require predefined expected outcomes, snapshot testing uses the current application state as a baseline for future comparisons, making it suitable for dynamic and complex scenarios.
  • Implementing snapshot testing in PHP can be done using the PHPUnit Snapshot Assertions library, which facilitates the creation and management of snapshot files during tests.
  • While snapshot testing reduces the effort in writing and maintaining tests and serves as a form of documentation, it can lead to outdated snapshots if not regularly updated, potentially causing false positives.
  • Snapshot testing is not a standalone solution but should be used in conjunction with other testing methods to ensure comprehensive coverage and reliability of software 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…

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.

Frequently Asked Questions (FAQs) on Snapshot Testing in PHP

What is Snapshot Testing in PHP?

Snapshot testing in PHP is a modern testing approach that captures the output or state of your application and compares it with a previously stored ‘snapshot’. This method is particularly useful for testing complex data structures like API responses, where manual testing can be time-consuming and error-prone. Snapshot testing automates this process, making it easier to identify and fix discrepancies in your code.

How does Snapshot Testing differ from traditional PHP testing methods?

Traditional PHP testing methods, such as unit testing, require you to know the expected outcome beforehand and write assertions based on that. Snapshot testing, on the other hand, allows you to capture the current state of your application and use it as a reference for future tests. This makes it a powerful tool for testing complex data structures and user interfaces, where predicting the exact output can be challenging.

How can I implement Snapshot Testing in my PHP project?

To implement snapshot testing in your PHP project, you can use libraries like PHPUnit Snapshot Assertions. This library provides a set of assertions for snapshot testing. You can install it via Composer and use it in your PHPUnit tests. When you run a snapshot test for the first time, it will create a snapshot file with the current output. In subsequent tests, it will compare the current output with this snapshot and alert you if there are any differences.

What are the benefits of Snapshot Testing in PHP?

Snapshot testing offers several benefits. It can significantly reduce the time and effort required to write and maintain tests, especially for complex data structures. It also provides a visual representation of changes in your application, making it easier to understand the impact of your code changes. Moreover, snapshot tests can serve as documentation, showing the expected output of your application at different points in time.

Are there any drawbacks to Snapshot Testing?

While snapshot testing is a powerful tool, it’s not without its drawbacks. One potential issue is that snapshots can become outdated if not updated regularly, leading to false positives in your tests. Additionally, snapshot testing may not be suitable for all scenarios, such as when the output is non-deterministic or changes frequently. It’s important to use snapshot testing judiciously and in conjunction with other testing methods for the best results.

How can I update my snapshots in PHPUnit?

To update your snapshots in PHPUnit, you can use the --update-snapshots or -u command line option when running your tests. This will update all snapshots to match the current output of your tests. Be sure to review the changes carefully before committing them, as this will become the new expected output for your tests.

Can I use Snapshot Testing for testing user interfaces in PHP?

Yes, snapshot testing can be used for testing user interfaces in PHP. This can be particularly useful for testing HTML output or JSON responses in web applications. By capturing a snapshot of the expected output, you can easily detect any changes or regressions in your user interface.

How does Snapshot Testing handle failures?

When a snapshot test fails, it means that the current output of your test does not match the stored snapshot. The test runner will typically show a diff of the changes, allowing you to easily see what has changed. If the changes are intentional, you can update the snapshot to match the new output. If the changes are not expected, this indicates a bug in your code that needs to be fixed.

Can I use Snapshot Testing with other testing frameworks?

Yes, while this article focuses on using snapshot testing with PHPUnit, the concept can be applied to other testing frameworks as well. Many modern testing frameworks, including Jest for JavaScript and PyTest for Python, support snapshot testing either natively or through additional libraries.

Is Snapshot Testing a replacement for other types of testing?

No, snapshot testing is not a replacement for other types of testing. It is a complementary tool that can be used alongside unit tests, integration tests, and end-to-end tests. While snapshot testing can simplify testing for certain scenarios, it’s important to use a mix of testing methods to ensure comprehensive coverage of your code.

Christopher PittChristopher Pitt
View Author

Christopher is a writer and coder, working at Over. He usually works on application architecture, though sometimes you'll find him building compilers or robots.

BrunoSJestOOPHPPHPReactSeleniumSnapshotsnapshot testingtddTesting
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week