What Is Snapshot Testing, and Is It Viable in PHP?
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.
Continue reading %What Is Snapshot Testing, and Is It Viable in PHP?%