Mocking is great for very lean unit tests. It allows you to setup a repeatable scenario to test as many of the states of a unit as you want. In general, I want to be testing only a single method or a single class. If you’re testing more than this, you’re not writing unit tests – you’re writing integration tests! A good measure on if you are writing proper unit tests would be to change something in the code to make a test fail. If you end up with a cascade of tests fail, you have integration tests, not unit tests!
So in unit testing, you want to be able to isolate a single unit and manipulate the inputs to cover all types of conditions and verify the validity of the outputs to those conditions. I like to think of it like an algebraic inequality problem. You always check your boundaries and then a value within the different boundary regions. Think of it similarly with unit testing.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| // Calculator.php
<?php
class Calculator {
public getNumberFromUserInput() {
// complicated function to get number from user input
}
public divideBy($num2) {
return $this->getNumberFromUserInput()/$num2;
}
}
// CalculatorTest.php
<?php
include_once("Calculator.php");
class CalculatorTest extends \PHPUnit_Framework_TestCase {
public function testDivideByPositiveNumber() {
$calcMock=$this->getMock('\Calculator',array('getNumberFromUserInput'));
$calcMock->expects($this->once())
->method('getNumberFromUserInput')
->will($this->returnValue(10));
$this->assertEquals(5,$calcMock->divideBy(2));
}
public function testDivideByZero() {
$calcMock=$this->getMock('\Calculator',array('getNumberFromUserInput'));
$calcMock->expects($this->once())
->method('getNumberFromUserInput')
->will($this->returnValue(10));
$this->assertEquals(NAN, $calcMock->divideBy(0));
}
public function testDivideByNegativeNumber() {
$calcMock=$this->getMock('\Calculator',array('getNumberFromUserInput'));
$calcMock->expects($this->once())
->method('getNumberFromUserInput')
->will($this->returnValue(10));
$this->assertEquals(-2,$calcMock->divideBy(-5));
}
} |
As you can see with the example, sometimes inputs and outputs of functions are not always so straight-forward. While we can have our standard input and output passed from the method parameters and the return value, more often than not, inputs and outputs are received and sent by calling other functions. With mocking though, this is no issue. We can mock any function and define exactly what it should return. We can also test and verify that the function was called.
Let’s break down the example.
1
| $calcMock=$this->getMock('\Calculator',array('getNumberFromUserInput')); |
We are creating a new Calculator mock. The first parameter tells phpunit what class to mock, the 2nd parameter tells phpunit to only mock the ‘getNumberFromUserInput’ function and not anything else. We need to use the real divideBy() function to test that. getMock() has a lot of useful options, refer to the api or Mark Mzyk’s blog post I found on the getMock method signatures.
The next line(s) set up the mocked function.
1
2
3
| $calcMock->expects($this->once())
->method('getNumberFromUserInput')
->will($this->returnValue(10)); |
Line 1: We are saying here that we expect this method to be called once. If it is called less or more than once, we will get an exception.
Line 2: The method name we are mocking out.
Line 3: The return value of the mocked out function. You can also throw exceptions, return back one of the arguments unmodified, or even call another callback function. The code for that is $this->throwException(new Exception()), $this->returnArgument($ArgumentNumber) and $this->returnCallback($callbackMethod) respectively. With the callback, all the parameters you pass to the mock will be passed to the callback.
Anyways, back to our example, I ran the tests and we see at our 0 boundary that we get behavior that we were not expecting and our tests fail.
1
| PHPUnit_Framework_Error_Warning : Division by zero |
Let’s fix that up.
1
2
3
4
5
6
7
8
9
10
| class Calculator {
public function getNumberFromUserInput() {
// complicated function to get number from user input
}
public function divideBy($num2) {
if ($num2 == 0) return NAN;
return $this->getNumberFromUserInput()/$num2;
}
} |
A few more failures…
PHPUnit_Framework_ExpectationFailedException : Expectation failed for method name is equal to when invoked 1 time(s).
Method was expected to be called 1 times, actually called 0 times.
And finally…
1
2
3
4
5
6
7
| public function testDivideByZero() {
$calcMock=$this->getMock('\Calculator',array('getNumberFromUserInput'));
$calcMock->expects($this->never())
->method('getNumberFromUserInput')
->will($this->returnValue(10));
$this->assertEquals(NAN, $calcMock->divideBy(0));
} |
There are many options that you can apply to expects. Refer to this table: http://www.phpunit.de/manual/3.0/en/mock-objects.html#mock-objects.tables.matchers Oddly I could not find the same documentation on the current version of phpunit.
Lastly, if our calculator outputted data via a function, we can still test the output by mocking the output function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| // Calculator.php
class Calculator {
public function getNumberFromUserInput() {
// complicated function to get number from user input
}
public function printToScreen($value) {
// another complicated function
}
public function divideBy($num2) {
if ($num2 == 0) $this->printToScreen("NaN");
$this->printToScreen($this->getNumberFromUserInput()/$num2);
}
// CalculatorTest.php
..
public function testDivideByPositiveNumber() {
$calcMock=$this->getMock('\Calculator',array('getNumberFromUserInput', 'printToScreen'));
$calcMock->expects($this->once())
->method('getNumberFromUserInput')
->will($this->returnValue(10));
$calcMock->expects($this->once())
->method('printToScreen')
->with($this->equalTo('5'));
$calcMock->divideBy(2);
}
.. |
->with() will test the that the method is called with parameters passed. If your original function has multiple parameters, just add them in as multiple arguments to with(). ->with($this->equalTo($param1), $this->anything(),$this->equalTo($param3)). You can use any of the constraints that phpunit supports - http://www.phpunit.de/manual/3.2/en/api.html#api.assert.tables.constraints
You can git clone this full example on my github gist – https://gist.github.com/4558701
So with that said, test your code! With mocking, it makes it dead simple to test things. There should be no excuse to having tests on all the aspects of your code.