PHPUnit 4.5 and Prophecy
PHPUnit 4.5 has been released today. The most notable change in this new version of PHPUnit is the out-of-the-box support for Prophecy, a modern object mocking framework for PHP.
Spoilt for Choice? – PHP Mocking Frameworks
PHPUnit has had built-in support for creating test doubles for many years. This implementation was originally inspired by the first generation of mocking frameworks for Java. Since then mocking frameworks have evolved. Modern mocking frameworks are more intuitive to use, lead to more readable code, and may even allow for a clear separation of a test double's configuration and the actual test double object itself.
Like many users of PHPUnit I am not satisfied with the API of PHPUnit's own mocking framework. This dissatisfaction has lead to the development of alternative mocking frameworks for PHP such as Mockery, Phake, or Prophecy. If I were to create a new mocking framework today it would probably look a lot like Prophecy. Which is why PHPUnit 4.5 introduced out-of-the-box support for it.
At the moment there are no plans to deprecate the API of PHPUnit's own mocking framework. It is likely, though, that at some point in the future we will encourage developers to use Prophecy's API instead of the "classic" PHPUnit API. As you can use PHPUnit's own mocking framework and Prophecy within the same test suite (yes, you can even also use Mockery and Phake at the same time) there is no need to migrate existing tests to Prophecy's API should you decide to use it for new tests. Also note that there are some features such as test proxies in PHPUnit's own mocking framework that are outside the scope of the Prophecy project.
Prophets, Prophecies, and Revelations
At the core of Prophecy's philosophy are the terms prophet, prophecy, and revelation:
"Prophecy has been named that way because it concentrates on describing the future behavior of objects with very limited knowledge about them. But as with any other prophecy, those object prophecies can't create themselves – there should be a Prophet."
The prophet creates prophecies by prophesizing them. Test double objects are then created by revealing their respective prophecy. You can think of a prophecy as the configuration for a test double that is stored in an object separate from the object (revelation) that acts as the test double.
Confused? I know that I was when I first read the introduction to Prophecy. But allow yourself some time to get to grips with this terminology. It will make sense, and prove useful, eventually.
Dummies, Stubs, and Mocks
For the following examples we use the Reader
interface and the Processor
class shown below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<?php
interface Reader { public function read ( ) ; } class Processor { public function process ( Reader $reader ) { $reader -> read ( '123' ) ; // ... } } |
Dummy
We need to pass a Reader
object to process()
when we want to test this method of the Processor
class. If, for the purpose of our test, we do not care about this Reader
object then we can get away with just passing a dummy object that implements the Reader
interface but that does not do anything:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<?php
class ProcessorTest extends PHPUnit_Framework_TestCase { public function testDataFromReaderCanBeProcessed ( ) { $reader = $this -> prophesize ( 'Reader' ) ; $processor = new Processor ; $processor -> process ( $reader -> reveal ( ) ) ; // ... } } |
In the code above, $this->prophesize('Reader')
creates a prophecy for our Reader
object. This object is used to configure our test double. Once we are done configuring the test double (in this first example we just need a dummy object without configuration) we use $reader->reveal()
to reveal the prophecy and create the actual test double object.
Stub
If, for the purpose of our test, we need the read()
method of the Reader
object to return a specific value then we need to use a stub:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<?php
class ProcessorTest extends PHPUnit_Framework_TestCase { public function testDataFromReaderCanBeProcessed ( ) { $reader = $this -> prophesize ( 'Reader' ) ; $reader -> read ( '123' ) -> willReturn ( 'value' ) ; $processor = new Processor ; $processor -> process ( $reader -> reveal ( ) ) ; // ... } } |
The $reader->read('123')->willReturn('value');
line configures the stub to return the string value
when the read()
method is called with '123'
as its argument.
Mock
If, for the purpose of our test, we need to ensure that the read()
method of the Reader
object is invoked with a specific argument from the unit of code under test then we need to use a mock:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<?php
class ProcessorTest extends PHPUnit_Framework_TestCase { public function testDataFromReaderCanBeProcessed ( ) { $reader = $this -> prophesize ( 'Reader' ) ; $reader -> read ( '123' ) -> willReturn ( 'value' ) -> shouldBeCalled ( ) ; $processor = new Processor ; $processor -> process ( $reader -> reveal ( ) ) ; // ... } } |
If the process()
method of the Processor
class does not invoke the read()
method on the mock as we expect it to then we get the output shown below:
PHPUnit 4.5.0 by Sebastian Bergmann and contributors. F Time: 42 ms, Memory: 3.75Mb There was 1 failure: 1) Test::testDataFromReaderCanBeProcessed Some predictions failed: Double\Reader\P1: No calls been made that match: Double\Reader\P1->read(exact("123")) but expected at least one. FAILURES! Tests: 1, Assertions: 1, Failures: 1.
Behind the scenes, Prophecy generated code for the test double in the Double\Reader\P1
class. The Double
namespace is used to hold such generated classes, Double\Reader
is the namespace for such generated classes for our Reader
class, and Double\Reader\P1
is the name of the generated class for the first (and in our example only) prediction.