One of the toughest things about unit testing is isolating the component you want to test. In order to do this, you must create a virtual world for your component to exist within – like The Matrix with assertions. And how do we create this sci-fi wonder? The answer lies with PHPUnit mock objects (also known as stubs or test doubles). Before I get into the details, here’s a cautionary tale about testing against real dependencies:
A developer was once tasked with creating a system to manage various marketing tasks for his employer – a large bank. The system was designed to despatch postal campaigns, SMS ads, newsletters and the like. A good coder, he had caught the testing bug, and was all on board for fleshing out the system’s unit test suite. So he put the main event manager through its paces. He generated some profiles based on the different bands of customers the department targeted – School Leaver, Hungry Student, Public Servant, Young Professional and so on. He wrote a test that passed these along to the event component’s main
action()
method then checked on the results. He ran the test on Wednesday evening. All went well, it seemed.It wasn’t until midday Friday that he learned of his development machine’s misconfiguration. His test had interacted with live systems. It called a component which merged a marketing mail with test data, printed letters then issued to the order to have them pushed out into the postal system. On Friday morning, two thousand of the bank’s most valuable customers opened a letter that began “Dear Rich Bastard”…
Things aren’t usually as dramatic as that – but the story does illustrate a key point. As far as possible a unit test should focus on a single component. Other code should be kept to a minimum. In this post I’m going to run through some of the ways that we can create PHPUnit mock objects that can be configured to fool a component into believing they are the real thing.
A class to test and a bystander
Let’s begin with a class named Context
:
class Context
{
function perform($input)
{
// work with databases and services
// and complicated stuff you don't want to run
// in a test
return $input;
}
}
As you can see, Context
does very little. Imagine, though, that it is the live component our hapless coder should not have invoked. The component we want to test will use Context
. As testers, we want to check on that interaction, but we don’t want Context
to be run at all. Here is the component we wish to test.
class MyCommand
{
function execute(Context $context, $input = 0)
{
// do stuff
try {
$importantvalue = $context->perform($input);
// and more stuff with $importantvalue
return ($importantvalue + 5);
} catch (\Exception $e) {
return -1;
}
}
}
MyCommand
has an execute()
method which is given a Context
object. It duly calls its perform()
method. Then it acts on the return value by adding five to it. If the Context
object throws an exception, the execute()
method recovers and returns -1.
The PHPUnit boilerplate
Here is the unit test class I’ll be using for this example:
class MyCommandTest extends \PHPUnit_Framework_TestCase
{
function testMock()
{
// code goes here
}
}
PHPUnit will run the testMock method for me. I’ll check that on the commandline:
$ phpunit test/MyCommandTest.php
PHPUnit 5.5.4 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 271 ms, Memory: 2.75MB
OK (1 test, 0 assertions)
Creating PHPUnit mock objects that returns a single value
Remember that it’s the Context
class that we want to fake. We can do that very simply with PHPUnit by calling createMock
and passing it a class name. Let’s do it
$mockcontext = $this->createMock(Context::class);
I now have a fake Context
object. I still need to configure it so that perfom()
returns something when called.
$mockcontext->method("perform")->willReturn(5);
The method()
method returns an kind of Swiss army knife for managing return values and behaviour (PHPUnit_Framework_MockObject_InvocationMocker
– though I won’t get too deep into the weeds). Perhaps the simplest of these tools is willReturn()
. By calling it I ensure that any time my Context
object’s perform()
is called it will return 5
.
Time to see if it works:
$mycommand = new MyCommand();
$testval = $mycommand->execute($mockcontext);
self::assertEquals(10, $testval);
Because MyCommand::execute()
adds 5 to the value returned by its Context
object, I am expecting the return value to be 10
– so that’s what I plug in to my assertion.
Returning different values for multiple calls
The previous example is fine if I want the same value returned every time Context::perform()
is called. But what if I want three different values for three separate calls? Simple:
$mockcontext = $this->createMock(Context::class);
$mockcontext->method("perform")->will(
$this->onConsecutiveCalls(2, 4, 6)
);
A slightly different structure for configuration. The will()
method accepts a range of different objects that set up different configurations. The conConsecutiveCalls()
method returns PHPUnit_Framework_MockObject_Stub_ConsecutiveCalls
– and that does what it says on the tin – ensures that consecutive calls yield the given values.
Here are my tests:
$testval = $mycommand->execute($mockcontext);
self::assertEquals(7, $testval);
$testval = $mycommand->execute($mockcontext);
self::assertEquals(9, $testval);
$testval = $mycommand->execute($mockcontext);
self::assertEquals(11, $testval);
If Context::perform()
returns 2, 4 and 6, I’m expecting MyCommand::execute()
to return 7,9, and 11 – and that’s what I get.
Making a stub object’s method throw an Exception
As well as testing operation, we often need to test a components ability to handle error conditions. Here’s how you can make a stub object throw an Exception instead of returning a value:
$mockcontext = $this->createMock(Context::class);
$mockcontext->method("perform")->will(
$this->throwException(new \Exception())
);
$testval = $mycommand->execute($mockcontext);
self::assertEquals(-1, $testval);
Remember that if Context
throws an exception, MyCommand
catches it and returns -1? This test confirms it.
Mapping arguments to return values in PHPUnit mock objects
If you need to make your PHPUnit mock objects return a particular value based on an argument (or a set of arguments) then you can use returnValueMap()
to set up your conditions. returnValueMap()
accepts an array of arrays. Each of the inner arrays should define begin with the expected argument or arguments, and end with the value you wish to be returned when a matching call is made.
$mockcontext = $this->createMock(Context::class);
$mockcontext->method("perform")->will(
$this->returnValueMap(
[
[ 3, 10 ],
[ 5, 15 ]
]
)
);
$testval = $mycommand->execute($mockcontext, 5);
self::assertEquals(20, $testval);
$testval = $mycommand->execute($mockcontext, 3);
self::assertEquals(15, $testval);
So, in this example, given an argument of 5
, my mock Context
will return 10
. Note that the order of the arrays is not important. Here, I call 5
first and then 3
even though the maps are defined the opposite way round.
Using callbacks to manage more complex logic
Although PHPUnit provides lots of convenient methods for returning values from mock objects, occasionally you’ll need to go the custom route. If you need more control, you can pass a callback to returnCallback()
:
$mockcontext = $this->createMock(Context::class);
$mockcontext->method("perform")->will(
$this->returnCallback(function ($arg) {
if ($arg == 12) {
throw new \Exception();
} else {
return 55;
}
})
);
$testval = $mycommand->execute($mockcontext, 12);
self::assertEquals(-1, $testval);
$testval = $mycommand->execute($mockcontext, 6);
self::assertEquals(60, $testval);
This allows me make my mock object’s perform()
method more sophisticated. I can test any arguments provided and craft my return value accordingly. In this case, if the argument is 12
I throw an exception, otherwise I return 55
. My tests confirm that MyCommand::execute()
returns -1
on encountering an exception, and adds five to any other value returned by perform()
(and in this example, the return value is always 55
).
There’s much more to testing with PHPUnit mock objects, of course, but since I have to look this return value stuff up myself every month or so, I thought I’d leave it here for my own use. I’ll return to the topic in later posts.
Image: _
“Models” – William Warby – license
CC BY 2.0_