7. Mocking systems

Mocks are virtual class, created on the fly. They are used to isolate your test from the behaviour of the other classes. atoum has a powerful and easy-to-implement mock system allowing you to generate mocks from classes or interfaces that exist, are virtual, are abstract or an interface.

With these mocks, you can simulate behaviours by redefining the public methods of your classes. For the private & protected method, use the visibility extension.

Warning

Most of method that configure the mock, apply only for the next mock generation!

7.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.1. Generate a mock with newMockInstance

If you prefer there is method called newMockInstance() that will generate a mock.

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

// is equivalent to
$this->newMockInstance('Countable');

Note

Like the mock generator, you can give extra parameters: $this->newMockInstance('class name', 'mock namespace', 'mock class name', ['constructor args']);

7.2. The mock generator

atoum relies on a specialised components to generate the mock: the mockGenerator. You have access to the latter in your tests in order to modify the procedure for the 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.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.

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;

Note

All what’s described here with the mock generator can be apply with newMockInstance

7.2.2. Shunt calls to parent methods

7.2.2.1. shuntParentClassCalls & unShuntParentClassCalls

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.

Note

shuntParentClassCalls will only be applied to the next generated mock. But if you create two mock of the same class, both will have they parent method shunted.

7.2.2.2. shunt

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;

A shunted method, will have empty method body but like for shuntParentClassCalls the signature of the method will be the same as the mocked method.

7.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. All the parameters of the method will also set as default value null. So it’s the same a shunted method, but with all parameter as null.

<?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);

Note

orphanize will only be applied to the next generated mock.

7.3. Modify the behaviour of a mock

Once the mock is 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:

  • $yourMock->getMockController()->yourMethod
  • $this->calling($yourMock)->yourMethod
<?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 connects 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

7.3.1. Change mock behaviour on multiple calls

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 behaviour, 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 behaviour, you can use the methods<mock_methods> or methodsMatching<mock_method_matching>.

7.3.2. methods

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

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

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

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

In the last example, you should instead use methodsMatching<mock_method_matching>.

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.3.3. 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 behaviour
$this
    ->calling($mock)
        ->methodsMatching('/^is/')
            ->return = true
;

// if the method starts by "get" (case insensitive),
// we redefines its behaviour
$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.3.4. isFluent && returnThis

Defines a fluent method, so the method return the class.

<?php
        $foo = new \mock\foo();
        $this->calling($foo)->bar = $foo;

        // is the same as
        $this->calling($foo)->bar->isFluent;
        // or this other one
        $this->calling($foo)->bar->returnThis;

7.3.5. doesNothing && doesSomething

Change the behaviour of the mock with doesNothing, the method will simply return null.

<?php
        class foo {
                public function bar() {
                        return 'baz';
                }
        }

        //
        // in your test
        $foo = new \mock\foo();
        $this->calling($foo)->bar = null;

        // is the same as
        $this->calling($foo)->bar->doesNothing;
        $this->variable($foo->bar())->isNull;

        // restore the behaviour
        $this->calling($foo)->bar->doesSomething;
        $this->string($foo->bar())->isEqualTo('baz');

Like you see in this example, if for any reason, you want to restore the behaviour of the method, use doesSomething.

7.3.6. 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($args)
{
     // do something with the args
};

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

For simple case you can use orphanize(‘__constructor’) or shunt(‘__constructor’).

7.4. 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.5. The mocking (mock) of native PHP functions

atoum allows to easily simulate the behaviours 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.6. 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. Here is an example:

<?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)
            ;
        }
    }
}