Using PHP test double patterns with Prophecy
Test doubles are a way of replacing all classes in our tests other than the one we are testing. They give us control on direct and indirect outputs that would have been generated by those classes.
Understanding the role of the test double helps us to write better tests, and thus better code.
If we manage to isolate the 'collaborators' (the other objects involved in our test) using doubles and test our objects in isolation, then we have achieved modularity; our code is decoupled and the communication between objects is documented by our tests.
When writing unit tests we are focusing on describing one unit of behaviour at a time, and how objects communicate in order to achieve that behaviour. We instantiate the object under test, call a method of the object and observe if the outcome is as expected.
Defining behaviour
We have used the word behaviour a couple of times already, so this seems like a good time to define it. By behaviour, at the object level, I merely mean the rules by which the object will convert input into output. The way we describe that behaviour using tests will depend on what kind of behaviour happens within that method call.
If I ask you, what can happen within a method? Your initial answer will likely be 'well, a million things!'. But if we give it some further thought, thinking of behaviour in terms of which resulting output should come from a given input, it boils down to a very narrow list:
- Throw an exception or produce an error
- Output something
- Modify the value of a property
- Return a value
- Call a method on a collaborator
Remember, behaviours are the rules regarding how input is processed to generate output, so we can even narrow down this list a bit more, as modifying a property doesn't really generate output, it's a middle step. It is also rare to output from code directly using print or echo, usually there will be a presentation layer to take care of that. Our list therefore becomes something like this:
- Throw an exception or produce an error √
- Output something to the standard output ✗
- Modify the value of a property ✗
- Return a value √
- Call a method on a collaborator √
Testing for exceptions is done by expressing in the test that the method call will generate an exception. We test a return value by matching it against an expected value. So let us focus now on the trick part, the 'delegation', or calling a method on a collaborator.
A test double pattern such as Mock Objects can help us with this task, and we will look in detail at these patterns shortly. First we will examine some of the other reasons we need test doubles, such as the fact that using real collaborators has some disadvantages:
- They are expensive to create
- They make the test slow
- They have internal behaviour that we cannot control
- They produce by-products that may interfere with other tests
- They need to return a specific value for us to describe the object under test
Types of doubles
In his book xUnit Test Patterns, Gerard Meszaros describes various test doubles patterns. Let's examine how we can implement some of these in PHP. I will be using PHPUnit and the PHP mocking framework Prophecy for my examples.
Prophecy integrates with PHPUnit thanks to the Prophecy-PHPUnit contribution of Christophe Coevoet, which you can find in Packagist. Here is the composer installation:
{
"require-dev": {
"phpunit/phpunit": "3.7.*@dev",
"phpspec/prophecy-phpunit": "dev-master"
},
"config": {
"bin-dir": "bin"
}
}
With this in place, we can go on and look at specific examples of the various types of test doubles, and when they are useful.
Dummy
Sometimes we use double simply to bypass PHP's typehinting. We just need an object of a certain type. This example of a very barebones double, without behaviour, is known as a Dummy. Here's an example of a Dummy in use.
<?php
use Prophecy\PhpUnit\ProphecyTestCase as TestCase;
class MarkdownTest extends TestCase {
/** @test */
function it_attaches_default_events()
{
$eventDispatcher = $this->prophesize('MarkdownEventEventDispatcher');
$markdown = new Markdown($eventDispatcher->reveal());
// continue with some assertions on $markdown...
}
}
The prophesize() method creates a Prophet object which we configure to control its direct and indirect outputs. For a Dummy we need not worry about any configuration, in this case we just need it to pretend to be an EventDispatcher, so we simply specify which class it should be and then call reveal() to create the double.
Fake
As we said before, sometimes calling the real collaborating object may result in unwanted side-effects. We don't want the system to break if our object calls a method on a collaborator, but we don't want to use the collaborator itself. The output of the method isn't important, we only care that we won't get an error when calling the method on a double. In this case a Dummy is not enough, we need a Fake.
<?php
use Prophecy\PhpUnit\ProphecyTestCase as TestCase;
use Prophecy\Argument;
class MarkdownTest extends TestCase
{
/** @test */
function it_attaches_default_events()
{
$eventDispatcher = $this->prophesize('Markdown\Event\EventDispatcher');
$eventDispatcher->addListener(Argument::type('Markdown\Event\EndOfLineListener'));
$eventDispatcher->addListener(Argument::type('Markdown\Event\EndOfListListener'));
$markdown = new Markdown($eventDispatcher->reveal());
// continue with some assertions on $markdown...
}
}
Unlike some other frameworks, Prophecy will not tolerate faking a method that hasn't been declared anywhere, so you will have to have those methods on your class or interface before attempting to make your test green.
The rationale behind forcing doubles to have the methods is that testing with doubles can sometimes give you a false sense that the object under test is working, when in fact the protocol it establishes with its collaborators is shallow. If you have experience of Ruby and RSpec, this is the behaviour you would get with rspec-fire.
Stub
Sometimes we need to control the indirect outputs inside a test run, or in other words, the behaviour of a collaborator. A Stub is a double that allows you to control its indirect outputs when describing the object under test.
Suppose we want to describe a behaviour in our EndOfListListener. The behaviour is: I will append closing <li> and <ul> HTML tags when I reach the end of a markdown list. I have an event passed to me, so I will need the event to return the text of the current line. Also my listener keeps a reference to the context, in this case the document, from which I can check if the next line is empty. So I need to control the indirect output of Event and Document.
Here is the code I am describing:
<?php
namespace Markdown\Event;
use Markdown\Document;
class EndOfListListener implements Listener
{
private $document;
public function __construct(Document $document)
{
$this->document = $document;
}
public function onNewLine(Event $event)
{
$html = $event->getText();
if ($this->document->nextLineIsEmpty()) {
$html .= "<ul><li>";
}
return $html;
}
}
The behaviour of onNewLine() depends on the behaviour of Event::getText() and Document::nextLineIsEmpty(). In other words, if I want to test the method, then I need to control what these methods do, by stubbing them. In the next example we stub the return value of the methods by appending the method calls with a call to willReturn().
<?php
use Prophecy\PhpUnit\ProphecyTestCase as TestCase;
use ProphecyArgument;
use MarkdownEventEndOfListListener;
class EndOfListListenerTest extends TestCase {
/** @test */
function it_ends_list_when_next_is_empty()
{
$event = $this->prophesize('Markdown\Event\Event');
$document = $this->prophesize('Markdown\Document');
$event->getText()->willReturn('Some text');
$document->nextLineIsEmpty()->willReturn(true);
$listener = new EndOfListListener($document->reveal());
$html = $listener->onNewLine($event->reveal());
$this->assertSame('Some text</li></ul>', $html);
}
}
Mock
We have seen already that Dummies are doubles with no behaviour, and Fakes and Stubs are doubles used to control indirect output. The third family of doubles serve yet another purpose: describing the protocols between objects. That is, if part of the behaviour of the method is delegated to a method in another object, we can use Mocks orSpies to verify if that method was called as expected. To add a mock expectation to your double in Prophecy you can append shouldBeCalled() to the method call.
Let's consider this scenario: A parser can have a few subscribers. When something happens to the parser, a notify() method is triggered. The behaviour of the method is to iterate through all the attached subscribers and invoke their onChange() method. Our code will look something like this:
<?php
namespace Markdown\Parser;
use Markdown\Event\Event;
class ParserSubject
{
// ...
public function notify(Event $event)
{
foreach ($this->subscribers as $subscriber) {
$subscriber->onChange($event);
}
}
}
We know that notify() works as expected because a Subscriber::onChange() of an attached subscriber gets called too. So, let's describe this.
<?php
use MarkdownParserParserSubject;
use ProphecyPhpUnitProphecyTestCase as TestCase;
class ParserSubjectTest extends TestCase
{
/** @test */
function it_notifies_an_attached_subscriber()
{
$parser = new ParserSubject;
$dummyEvent = $this->prophesize('MarkdownEventEvent')->reveal();
$subscriber = $this->prophesize('MarkdownParserSubscriber');
$subscriber->onChange($dummyEvent)->shouldBeCalled();
$parser->notify($dummyEvent);
}
}
Demeter violations
Back in 1988 a paper was published with the title 'Object-Oriented Programming: An Object Sense of Style'. The document describes in depth the motivations for the Law of Demeter, which in layman's terms means 'inside a method you should only call the methods of the class collaborators, and not the methods of the collaborators of the collaborator'. Let's see an example of a violation of this principle:
<?php
if ($document->getNextLine()->getText()->IsEmpty()) {
$html .= "<ul><li>";
}
To stub a chain like that would mean that you would need to stub the three methods, and make a stub return another stub consecutively. Test frameworks exist that solve that problem elegantly, but Prophecy will not.
Corey Haines points out that the difference between test-driven design and test-first development lies in what you do when writing your tests becomes difficult.
If you install helpers or use macros or think of a work around to make the testing easier you are doing test-first. If, however, when you come across some hard-to-test piece of code and the first thing you think of is to refactor the code into a better design, that's when you are driving the design with the tests.
If you encounter with code that violates the Law of Demeter you should try and keep the data and behaviour close together. Move the method to where the data is, or vice versa: have this object as you collaborator and call it directly.
<?php
if ($document->nextLineIsEmpty()) {
$html .= "<ul><li>";
}
The benefit of this approach, apart from making your life a lot easier when you are testing, is that when the lower level implementation changes (in this example how you check if the next line is empty), you won't have to change the classes above which are unaware of how we verify this. Using all those getters will cause you to change in all the places you've used them, making your code much more brittle.
Don't mock what you don't own
This principle I learned from 'Growing Object-Oriented Software, Guided by Tests'. To mock code I don't own (code in a PHP framework, the ORM layer, or a third party API) leads me to feel that I am in trouble for testing the behaviour I want to add. The code we don't own carries hidden complexity, that even if we have the source code available, may be very difficult to stub. The solution is to write an adapter layer.
Here is an example. Say you are adding some new functionality to the rating system of a website. The bit you are adding is that when someone clicks on the star it recalculates the average and fill the right number of stars. It will also fill quarters of a star if it has gone above the respective quarter.
To implement this you will need to query from some data source to know the number of times each item has been rated in each level, weight them accordingly and then calculate the average. The solution for this is to separate the rules for calculating the rating and the query using an adapter.
You can provide a high-level service that has a reference to the object which models rules and the object that access the data. You can unit test your rules and pick up integration issues with either acceptance tests, integration tests or both.
The use case rating service class would look something like:
<?php
class RatingService
{
private $ratingsRepository;
public function __construct(RatingsRepository $repository)
{
$this->ratingsRepository = $ratingRepository;
}
public function rate(Product $product, Rating $rating)
{
$this->ratingRepository->insertRating($product, $rating);
}
public function calculateAverage(Product $product)
{
$productRatings = $this->ratingRepository->findByProduct($product);
return $productRatings->calculateAverage();
}
}
Where the repository would look like:
<?php
interface RatingRepository
{
public function insertRating(Product $product, Rating $rating);
/**
* @param Product $product
* @return ProductRatings
*/
public function findByProduct(Product $product);
}
I have chosen to make our Rating a value object and ProductRatings the domain object that holds the business rules on how to calculate the average. RatingService is the use case, a high-level access point to the functionality.
By separating the code in this way you can unit test the business logic of your domain object and not worry about any database dependency. If queries are complex and worth verifying, integration tests can be added.
Integration tests become a problem when the high-level layer holds logic, as we are more likely to miss a scenario that way. By making sure the business logic is in domain object, we can more safely rely on integration tests for the outer layer, removing the need to mock the database when testing the service.
Using an adapter
When you are confronted with a situation where, for example, you are consuming a service API from a third party, you should still try to NOT mock the dependencies. To be able to unit test your classes you need to mock the dependencies, but since mocking is describing protocols between objects you need to trust the protocol will not be broken.
The solution to this problem suggested in the Growing Object Oriented Software book is to write an adapter layer. Your adapter will provide an access point to the external API.
When you create your adapter layer, you will then mock your adapter in order to test your classes. That, of course, doesn't provide the full confidence your service will always work. If the API changes, you will not pick this up with your tests. You should protect against that by adding integration tests that actually hit a testing server provisioned with the third party API. Here is an example of using an adapter to test code that relies on an external API:
<?php class ProductRatings
{
private $apiAdapter;
private $product;
private $ratings;
public function __construct(RatingAPIAdapter $apiAdapter, Product $product, array $ratings)
{
$this->apiAdapter = $apiAdapter;
$this->product = $product;
$this->ratings = $ratings;
}
public function calculateAverage()
{
$average = $this->apiAdapter->calculateAverage($this->ratings);
if ($this->product->isSponsored() && $average < 4) {
return 4;
}
return $average;
}
}
You can stub the adapter's calculateAverage() method like this:
<?php
use Prophecy\PhpUnit\ProphecyTestCase as TestCase;
use ProphecyArgument;
class ProductRatingsTest extends TestCase {
/**@ test */
function it_shows_an_average_of_at_least_4_if_product_is_sponsored()
{
$product = $this->prophesize('Product');
$product->isSponsored()->willReturn(true);
$adapter = $this->prophesize('RatingAPIAdapter');
$adapter->calculateAverage(Argument::any())->willReturn(3);
$productRatings = new ProductRatings($adapter->reveal(), $product->reveal(), array());
$average = $productRatings->calculateAverage();
$this->assertGreaterThanOrEqual(4, $average);
}
}
Wrapping up
Hopefully you found the examples in this article useful; and you find the techniques helpful in your own projects. You can find Prophecy on github. There you will also find more documentation and a patient and newcomer-friendly community.
Learning to use doubles is a very important skill as it makes you think about how objects collaborate, which, if you do well, will result in code that is much more maintainable, expressive and clean.