Stubs and mocks of bovigo-callmap used in PHPUnit

11/22/15

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.

Installation


As expected, the installation happens with Composer and also works fine with PHPUnit 5, because bovigo/callmap has no dependency to any version. That's why it is possible to use it with other testing frameworks too.
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.

What's the deal with bovigo/callmap?


We already have a lot of existing mock libraries, so why should someone use this addition to the mock-family for their project's tests?

Of course, it offers many features we already know from other mock objects, so I will mention the more memorable ones. The first difference of bovigo/callmap to other test doubles is, that the stubs/mocks are created as an array map, where the keys represent the method names and the values the method's return value. Mockery can do something similar, but the mapping's return values are very flexible in bovigo/callmap.

Let's take a first look on a simple example of how to use it in a PHPUnit test.
<?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.

While PHPUnit's own mock objects return null for any of its methods by default, 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().

Spice up return values


Sometimes one simple return value is not enough for your mocked methods. That's where the flexibility of bovigo/callmap comes quite handy: It allows to use callbacks, consecutive calls and Exceptions on method invocation, directly defined in 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:

  • throws - Needs an Exception object that's thrown on method invocation
  • onConsecutiveCalls - Gives Different return values in a specific order on every method invokation. Don't confuse it with the PHPUnit function with the same name
  • wrap - In case the return value should be a callable, you need to wrap it in this function


Fluid interfaces


There is more about 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.
On the other hand we depend now on a non-code part that is outside of our control in case of third party code. There are also some restrictions when this feature can be used and when annotations can't be read. For example, in the current version of bovigo/callmap the inheritDoc annotation is not supported and the Traversable interface doesn't apply either. But there's one thing we should looking forward to in the near future: PHP 7.

Coming soon?


Straight from the documentation: Btw: looking forward to return type hints with PHP 7. ;-)

PHP 7 is waiting in a not so distant future and bovigo/callmap may show its strength with the new features of the next major PHP release. Instead of annotations, the mock object can get its information about the return type from a method's definition. It may be another mocking library right now, but with the potential to become an interesting addition to unit tests, and with the start of PHP 7, we may see some new helpful features in future releases of bovigo/callmap. You can read more about it in the bovigo/callmap repository and try it out for yourself.

I'll keep an eye on that project. What about you?


About Claudio Zizza
I'm working as a software developer for more than 15 years and used many coding languages and frameworks during that time, like PHP (of course), ASP, .NET, Javascript, Symfony, Laminas and more. I also contribute to the open source communities and am a part of the PHP Usergroup in Karlsruhe, Germany.
Follow me on Twitter (@SenseException)