Categories
Uncategorized

Test spies in PHP

I think that I wrote my first unit tests in Ruby with RSpec, back in the day. But I learned most of my testing knowledge from working with mocha and chai in JavaScript. One of the things that I learned from these tools is that being able to express your test logic in clear, nearly-natural language brings a real comfort to the often complicated life of the test-writer.

For example, here’s a line straight from the chai home page:

expect(tea).to.have.property('flavors').with.length(3);

It’s so lovely.

When writing tests to follow the flow of code from one module to the next, it’s often useful to have Test Spies around to “spy” on the code and tell you what’s happening when. To borrow the definition from my favorite test spy library, sinon:

A test spy is a function that records arguments, return value, the value of this and exception thrown (if any) for all its calls. A test spy can be an anonymous function or it can wrap an existing function.

When brought together with sinon-chai, this allows for very expressive syntax:

expect(mySpy).to.have.been.calledWith("foo");

Because I work a lot with WordPress, I also write a lot of unit tests in PHP. There, the defacto standard is PHPUnit, which has… less natural language. That’s fine, though, its assertions are usually perfectly readable. What it seems to be lacking, for me anyway, is Spies.

Certainly there are some great ways to use PHPUnit’s built-in mocking tools, like this (hidden) behavior, but Spies are hardly a first-class citizen. And besides, even writing this feels confusing to me:

$target = $this->getMock('TargetClass');
$target->expects($spy = $this->any())->method('doSomething');

$target->doSomething("foo");
$target->doSomething("bar");

$invocations = $spy->getInvocations();

$this->assertEquals(2, count($invocations));

I would much rather write:

$target = mock_object_for('TargetClass');
$spy = $target->spy_on_method('doSomething');

$target->doSomething("foo");
$target->doSomething("bar");

expect_spy($spy)->to_have_been_called->twice();

So I ended up writing a Test Spy library for PHP. I call it Spies. My intent was to have an easy and readable way to create Test Spies, Stubs, and Mocks, and also a clear way to write expectations for them.

Creating a Spy is as simple as calling \Spies\make_spy(); (although there’s many other ways to create them). You can then call the spy and ask it questions, like this:

$spy = \Spies\make_spy();
$spy( 'hello', 'world' );

$spy->was_called(); // Returns true
$spy->was_called_times( 1 ); // Returns true
$spy->was_called_times( 2 ); // Returns false
$spy->get_times_called(); // Returns 1
$spy->was_called_with( 'hello', 'world' ); // Returns true
$spy->was_called_with( 'goodbye', 'world' ); // Returns false
$spy->get_call( 0 )->get_args(); // Returns ['hello', 'world']

Here’s a more complete example of using Spies to mock an object and test its behavior:


<?php
class GreetingGenerator {
public function get_greeting( $name ) {
// Assumes determine_greeting is defined in another file
return determine_greeting( $name );
}
}
class Greeter {
public function __construct( $greeting_generator ) {
$this->generator = $greeting_generator;
}
public function greet( $name ) {
return $this->generator->get_greeting( $name ) . ' ' . $name;
}
}
class GreeterTest extends PHPUnit_Framework_TestCase {
public function tearDown() {
\Spies\finish_spying();
}
public function test_greeter_greet_returns_result_of_get_greeting() {
$mock_generator = \Spies\mock_object_of( 'GreetingGenerator' );
$mock_generator->add_method( 'get_greeting' )->when_called->will_return( 'yo' );
$greeter = new Greeter( $mock_generator );
$this->assertEquals( 'yo joe', $greeter->greet( 'joe' ) );
}
public function test_greeter_passes_name_to_get_greeting() {
$mock_generator = \Spies\mock_object_of( 'GreetingGenerator' );
$spy = $mock_generator->spy_on_method( 'get_greeting' );
$greeter = new Greeter( $mock_generator );
$greeter->greet( 'joe' );
// Using an Expectation:
\Spies\expect_spy( $spy )->to_have_been_called->with( 'joe' );
// Or, without using an Expectation:
$this->assertTrue( $spy->was_called_with( 'joe' ) );
}
public function test_greeter_uses_return_value_of_determine_greeting() {
\Spies\stub_function( 'determine_greeting' )->when_called->will_return( 'my good friend' );
$greeter = new Greeter( new GreetingGenerator() );
$this->assertEquals( 'my good friend joe', $greeter->greet( 'joe' ) );
}
}

You can install Spies using Composer by typing composer require sirbrillig/spies in your project.

I owe quite a lot to the great library WP_Mock by 10up. Many of the concepts in Spies were inspired directly by the way that WP_Mock works, like creating global functions. WP_Mock serves a slightly different purpose, though. It’s a layer on top of Mockery, which in turn is a layer on top of PHP’s own testing tools. Also, it mocks a few things by default, like filters and actions.

Spies is intended to be more generally useful, and also to make the distinction between mocking and setting expectations more clear by breaking away from PHP’s built-in mocking concepts.

I welcome comments and suggestions at the Github page! As always, I hope it ends up being useful to other people.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s