7. Mocking systems

TODO get mock parts from everywhere + add stuff from issues

7.1. The mocks

atoum has a powerful and easy-to-implement mock system allowing you to generate mocks from (existing, nonexistent, abstract or not) classes or interfaces. With these mocks, you can simulate behaviors by redefining the public methods of your classes.

7.1.1. Generate a mock

There are several ways to create a mock from an interface or a class.

The simplest one is to create an object with the absolute name prefixed by mock:

<?php
// creation of a mock of the interface \Countable
$countableMock = new \mock\Countable;

// creation of a mock from the abstract class
// \Vendor\Project\AbstractClass
$vendorAppMock = new \mock\Vendor\Project\AbstractClass;

// creation of mock of the \StdClass class
$stdObject     = new \mock\StdClass;

// creation of a mock from a non-existing class
$anonymousMock = new \mock\My\Unknown\Claass;

7.1.2. The mock generator

atoum relies on a specialized component to generate the mock: the mockGenerator. You have access to the latter in your tests in order to modify the procedure for generation of the mocks.

By default, the mock will be generated in the “mock” namespace and behave exactly in the same way as instances of the original class (mock inherits directly from the original class).

7.1.2.1. Change the name of the class

If you wish to change the name of the class or its namespace, you must use the mockGenerator.

Its generate method takes 3 parameters:

  • the name of the interface or class to mock ;
  • the new namespace, optional ;
  • the new name of class, optional.
<?php
// creation of a mock of the interface \Countable to \MyMock\Countable
// we only change the namespace
$this->mockGenerator->generate('\Countable', '\MyMock');

// creation of a mock from the abstract class
// \Vendor\Project\AbstractClass to \MyMock\AClass
// change the namespace and class name
$this->mockGenerator->generate('\Vendor\Project\AbstractClass', '\MyMock', 'AClass');

// creation of a mock of \StdClass to \mock\OneClass
// We only changes the name of the class
$this->mockGenerator->generate('\StdClass', null, 'OneClass');

// we can now instantiate these mocks
$vendorAppMock = new \myMock\AClass;
$countableMock = new \myMock\Countable;
$stdObject     = new \mock\OneClass;

Note

If you use only the first argument and do not change the namespace or the name of the class, then the first solution is equivalent, easiest to read and recommended.

Note

You can access to the code from the class generated by the mock generator by calling $this->mockGenerator->getMockedClassCode(), in order to debug, for example. This method takes the same arguments as the method generate.

<?php
$countableMock = new \mock\Countable;

// is equivalent to:

$this->mockGenerator->generate('\Countable');   // useless
$countableMock = new \mock\Countable;

7.1.2.2. Shunt calls to parent methods

A mock inherits from the class from which it was generated, its methods therefore behave exactly the same way.

In some cases, it may be useful to shunt calls to parent methods so that their code is not run. The mockGenerator offers several methods to achieve this :

<?php
// The mock will not call the parent class
$this->mockGenerator->shuntParentClassCalls();

$mock = new \mock\OneClass;

// the mock will again call the parent class
$this->mockGenerator->unshuntParentClassCalls();

Here, all mock methods will behave as if they had no implementation however they will keep the signature of the original methods. You can also specify the methods you want to shunt :

<?php
// the mock will not call the parent class for the method firstMethod…...
$this->mockGenerator->shunt('firstMethod');
// ... nor for the method secondMethod
$this->mockGenerator->shunt('secondMethod');

$countableMock = new \mock\OneClass;

7.1.2.3. Make an orphan method

It may be interesting to make an orphan method, that is, give him a signature and implementation empty. This can be particularly useful for generating mocks without having to instantiate all their dependencies.

<?php
class FirstClass {
    protected $dep;

    public function __construct(SecondClass $dep) {
        $this->dep = $dep;
    }
}

class SecondClass {
    protected $deps;

    public function __construct(ThirdClass $a, FourthClass $b) {
        $this->deps = array($a, $b);
    }
}

$this->mockGenerator->orphanize('__construct');
$this->mockGenerator->shuntParentClassCalls();

// We can instantiate the mock without injecting dependencies
$mock = new \mock\SecondClass();

$object = new FirstClass($mock);

7.1.3. Modify the behavior of a mock

Once the mock created and instantiated, it is often useful to be able to change the behaviour of its methods.

To do this, you must use its controller using one of the following methods:

<?php
$mockDbClient = new \mock\Database\Client();

$mockDbClient->getMockController()->connect = function() {};
// Equivalent to
$this->calling($mockDbClient)->connect = function() {};

The mockController allows you to redefine only public and abstract protected methods and puts at your disposal several methods :

<?php
$mockDbClient = new \mock\Database\Client();

// Redefine the method connect: it will always return true
$this->calling($mockDbClient)->connect = true;

// Redefine the method select: it will execute the given anonymous function
$this->calling($mockDbClient)->select = function() {
    return array();
};

// redefine the method query with arguments
$result = array();
$this->calling($mockDbClient)->query = function(Query $query) use($result) {
    switch($query->type) {
        case Query::SELECT:
            return $result;

        default;
            return null;
    }
};

// the method connect will throw an exception
$this->calling($mockDbClient)->connect->throw = new \Database\Client\Exception();

Note

The syntax uses anonymous functions (also called closures) introduced in PHP 5.3. Refer to PHP manual for more information on the subject.

As you can see, it is possible to use several methods to get the desired behaviour:

  • Use a static value that will be returned by the method
  • Use a short implementation thanks to anonymous functions of PHP
  • Use the throw keyword to throw an exception

You can also specify multiple values based on the order of call:

<?php
// default
$this->calling($mockDbClient)->count = rand(0, 10);
// equivalent to
$this->calling($mockDbClient)->count[0] = rand(0, 10);

// 1st call
$this->calling($mockDbClient)->count[1] = 13;

// 3rd call
$this->calling($mockDbClient)->count[3] = 42;
  • The first call will return 13.
  • The second will be the default behavior, it means a random number.
  • The third call will return 42.
  • All subsequent calls will have the default behaviour, i.e. random numbers.

If you want several methods of the mock have the same behavior, you can use the methods or methodsMatching.

7.1.3.1. methods

methods allows you, thanks to the anonymous function passed as an argument, to define to what methods the behaviour must be modified :

<?php
// if the method has such and such name,
// we redefines its behavior
$this
    ->calling($mock)
        ->methods(
            function($method) {
                return in_array(
                    $method,
                    array(
                        'getOneThing',
                        'getAnOtherThing'
                    )
                );
            }
        )
            ->return = uniqid()
;

// we redefines the behavior of all methods
$this
    ->calling($mock)
        ->methods()
            ->return = null
;

// if the method begins by "get",
// we redefines its behavior
$this
    ->calling($mock)
        ->methods(
            function($method) {
                return substr($method, 0, 3) == 'get';
            }
        )
            ->return = uniqid()
;

In the last example, you should instead use methodsMatching.

Note

The syntax uses anonymous functions (also called closures) introduced in PHP 5.3. Refer to PHP manual for more information on the subject.

7.1.3.2. methodsMatching

methodsMatching allows you to set the methods where the behaviour must be modified using the regular expression passed as an argument :

<?php
// if the method begins by "is",
// we redefines its behavior
$this
    ->calling($mock)
        ->methodsMatching('/^is/')
            ->return = true
;

// if the method starts by "get" (case insensitive),
// we redefines its behavior
$this
    ->calling($mock)
        ->methodsMatching('/^get/i')
            ->throw = new \exception
;

Note

methodsMatching use preg_match and regular expressions. Refer to the PHP manual for more information on the subject.

7.1.4. Particular case of the constructor

To mock class constructor, you need:

  • create an instance of \atoum\mock\controller class before you call the constructor of the mock ;
  • set via this control the behaviour of the constructor of the mock using an anonymous function ;
  • inject the controller during the instantiation of the mock in the last argument.
<?php
$controller = new \atoum\mock\controller();
$controller->__construct = function() {};

$mockDbClient = new \mock\Database\Client(DB_HOST, DB_USER, DB_PASS, $controller);

7.1.5. Test mock

atoum lets you verify that a mock was used properly.

<?php
$mockDbClient = new \mock\Database\Client();
$mockDbClient->getMockController()->connect = function() {};
$mockDbClient->getMockController()->query   = array();

$bankAccount = new \Vendor\Project\Bank\Account();
$this
    // use of the mock via another object
    ->array($bankAccount->getOperations($mockDbClient))
        ->isEmpty()

    // test of the mock
    ->mock($mockDbClient)
        ->call('query')
            ->once() // check that the query method
                            // has been called only once
;

Note

Refer to the documentation on the mock for more information on testing mocks.

7.2. The mocking (mock) of native PHP functions

atoum allow to easyly simulate the behavious of native PHP functions.

<?php

$this
   ->assert('the file exist')
      ->given($this->newTestedInstance())
      ->if($this->function->file_exists = true)
      ->then
      ->object($this->testedInstance->loadConfigFile())
         ->isTestedInstance()
         ->function('file_exists')->wasCalled()->once()

   ->assert('le fichier does not exist')
      ->given($this->newTestedInstance())
      ->if($this->function->file_exists = false )
      ->then
      ->exception(function() { $this->testedInstance->loadConfigFile(); })
;

Important

The \ is not allowed before any functions to simulate because atoum take the resolution mechanism of PHP’s namespace.

Important

For the same reason, if a native function was already called before, his mocking will be without any effect.

<?php

$this
   ->given($this->newTestedInstance())
   ->exception(function() { $this->testedInstance->loadConfigFile(); }) // the function file_exists and is called before is mocking

   ->if($this->function->file_exists = true ) // the mocking can take the place of the native function file_exists
   ->object($this->testedInstance->loadConfigFile())
      ->isTestedInstance()
;

Note

Check the detail about isTestedInstance().

7.3. The mocking of constant

PHP constant can be declared with defined, but with atoum you can mock it like this:

<?php
$this->constant->PHP_VERSION_ID = '606060'; // troll \o/

$this
    ->given($this->newTestedInstance())
    ->then
        ->variable($this->testedInstance->hello())->isEqualTo(PHP_VERSION_ID)
    ->if($this->constant->PHP_VERSION_ID = uniqid())
    ->then
        ->variable($this->testedInstance->hello())->isEqualTo(PHP_VERSION_ID)
;

Warning, due to the nature of constant in PHP, following the engine you can meet some issue.

<?php

namespace foo {
    class foo {
        public function hello()
        {
            return PHP_VERSION_ID;
        }
    }
}

namespace tests\units\foo {
    use atoum;

    /**
     * @engine inline
     */
    class foo extends atoum
    {
        public function testFoo()
        {
            $this
                ->given($this->newTestedInstance())
                ->then
                    ->variable($this->testedInstance->hello())->isEqualTo(PHP_VERSION_ID)
                ->if($this->constant->PHP_VERSION_ID = uniqid())
                ->then
                    ->variable($this->testedInstance->hello())->isEqualTo(PHP_VERSION_ID)
            ;
        }

        public function testBar()
        {
            $this
                ->given($this->newTestedInstance())
                ->if($this->constant->PHP_VERSION_ID = $mockVersionId = uniqid()) // inline engine will fail here
                ->then
                    ->variable($this->testedInstance->hello())->isEqualTo($mockVersionId)
                ->if($this->constant->PHP_VERSION_ID = $mockVersionId = uniqid()) // isolate/concurrent engines will fail here
                ->then
                    ->variable($this->testedInstance->hello())->isEqualTo($mockVersionId)
            ;
        }
    }
}