When it comes to test doubles in unit tests, mock objects are the common way to replace a dependency in a tested unit. Most of us started to test PHP code with the testing framework PHPUnit that is shipped with its own mocking library. Even though it already offers a lot of possibilities to replace dependencies in unit tests, many other new innovative mocking libraries were released over the last years offering more features or an much easier API for developers. One of them even got into the default PHPUnit installation as a regular feature in version 4.5: Prophecy.
At my talk from the last meeting of the Karlsruher PHP Usergroup in October 2015, I made a short introduction of several mocking frameworks/libraries that are currently in use. One of them was new to me weeks ago and is a young project with its first commit in the beginning of April 2015. I will give a first impression of this project called bovigo/callmap.
composer require --dev bovigo/callmap
We are using it for dev only, so be sure to install it for the require-dev part of your composer.json. The examples in this article are written in PHP 5.6, so be sure to adapt your code if you're working with an older version.<?php
use bovigo\callmap\NewInstance;
use function bovigo\callmap\verify;
class ClassmapTest extends PHPUnit_Framework_TestCase
{
public function testSimpleMap()
{
$foo = NewInstance::of(Foo::CLASS, ['constructArgs'])
->mapCalls(
[
'bar' => 2,
'baz' => 3,
'qux' => 4,
'quux' => 5,
]
);
$this->assertEquals(2, $foo->bar());
$this->assertEquals(3, $foo->baz());
$this->assertEquals(4, $foo->qux());
$this->assertEquals(5, $foo->quux(0));
verify($foo, 'bar')->wasCalledOnce();
verify($foo, 'baz')->wasCalledOnce();
verify($foo, 'qux')->wasCalledOnce();
verify($foo, 'quux')->wasCalledOnce();
}
}
In the example above we created stubs at first from Foo class using NewInstance::of()
by adding all public methods in mapCalls()
as an array. The whole mocking experience happens after the methods were called and we check for the invocations. For this case bovigo/callmap provides a function called verify()
to check if the methods' invocations had the expected amount. An instance of bovigo\callmap\Verification
is created inside that function that can also verify arguments given to the methods. If an assertion of verify()
does not match, bovigo/callmap throws an Exception that is extending PHPUnit_Framework_ExpectationFailedException
when used in PHPUnit. Otherwise it's a regular Exception.NewInstance::of()
will delegate method calls to the original class, if the method is not covered in mapCalls()
. You should be aware of that partial behaviour and that classes may need a constructor argument like Foo in our example. In case an interface is used for a mock, the default return value is null. For a behaviour like PHPUnit's mock object and default null return values, you have to use NewInstance::stub()
.mapCalls()
.<?php
use bovigo\callmap\NewInstance;
use function bovigo\callmap\throws;
use function bovigo\callmap\onConsecutiveCalls;
$foo = NewInstance::of(Foo::CLASS, ['constructArgs'])
->mapCalls(
[
'bar' => function() { return 2; },
'baz' => onConsecutiveCalls(1, 2, 3),
'qux' => throws(new \Exception('Exception happens')),
'quux' => 'intval'
]
);
This unites the functionality we know of PHPUnit's mock object with a more readable code. bovigo/callmap has the following helper functions for return values:NewInstance::stub()
than only a default null value. If a method's DocBlock return annotation refers to itself, bovigo/callmap automagicly returns the mock or stub itself and makes it easy to build a test double for fluent interfaces or chained methods. Let's take Doctrine's QueryBuilder for a test example. Builder classes are known for methods that are returning the builder-instance.<?php
use bovigo\callmap\NewInstance;
use function bovigo\callmap\verify;
use Doctrine\DBAL\Query\QueryBuilder;
class ClassmapTest extends PHPUnit_Framework_TestCase
{
public function testFluentInterface()
{
$qb = NewInstance::stub(QueryBuilder::CLASS);
$qb->select('id', 'name', 'surname')
->from('users', 'u');
verify($qb, 'select')->wasCalledOnce();
verify($qb, 'from')->wasCalledOnce();
}
}
mapCalls()
is not used here but it is clearly visible, that the QueryBuilder's methods can be chained as long as the annotation of the methods have the QueryBuilder as return type. The amount of test code can be reduced that way and method-chains are easy to mock.