Back to Home

PHPUnit: Mock objects

php · phpunit · mock

PHPUnit: Mock objects

Quite often, when writing unit tests, we have to deal with the fact that the tested class depends on data from external sources, the state of which we cannot control. Such sources include a far-reaching public database or service, a sensor of some physical process, etc. Or we need to make sure that certain actions are performed in a strictly defined order. In these cases, Mock objects come to our aid (mock is a parody in English), allowing you to test classes in isolation from external dependencies. Use of Mock objects in PHPUnit is dedicated to this article.

As an example, we take the following class description:
class MyClass {
    protected function showWord($word) { /* отображает указанное слово на абстрактном устройстве */ }
    protected function getTemperature() { /* обращение к датчику температуры */ }
    public getWord($temparature) {
        $temperature = (int)$temparature;
        if ($temperature < 15) { return 'cold'; }
        if ($temperature > 25) { return 'hot'; }
        return 'warm';
    }
    public function process() {
        $temperature = $this->getTemperature();
        $word = $this->getWord($temperature);
        $this->showWord($word);
    }
}

Objects of this class are designed to display on one device one of the three weather conditions, depending on the ambient temperature. At the time of writing the code, neither the device for displaying the result, nor the temperature sensor are available, and an attempt to access them may lead to a program failure.

In the simplest case, to test the logic, we can inherit from the specified class, replace with stub methods that access unconnected devices, and conduct unit testing on the child instance. Mock objects are implemented in PHPUnit in about the same way, which provides additional convenience in the form of a built-in API.

Getting Mock Object


To get an instance of a Mock object, use the getMock () method:
class MyClassTest extends PHPUnit_Framework_TestCase {
    public function test_process() {
        $mock = $this->getMock('MyClass');
        // проверяем, что в $mock находится экземпляр класса MyClass
        $this->assertInstanceOf('MyClass', $mock);
    }
}

As you can see, getting the Mock object we need is very simple. By default, all methods in it will be replaced by stubs that do nothing and always return null.

GetMock call options

public function getMock(
    $originalClassName, // название оригинального класса, для которого будет создан Mock объект
    $methods = array(), // в этом массиве можно указать какие именно методы будут подменены
    array $arguments = array(), // аргументы, передаваемые в конструктор
    $mockClassName = '', // можно указать имя Mock класса
    $callOriginalConstructor = true, // отключение вызова __construct()
    $callOriginalClone = true, // отключение вызова __clone()
    $callAutoload = true // отключение вызова __autoload()
);

Passing getMock () to the builder as the second argument to null will cause the Mock object to be returned without any substitution at all.

getMockBuilder

For those who prefer to write in a chain style, PHPUnit offers an appropriate constructor:
$mock = $this->getMockBuilder('MyClass')
    ->setMethods(null)
    ->setConstructorArgs(array())
    ->setMockClassName('')
    // отключив вызов конструктора, можно получить Mock объект "одиночки"
    ->disableOriginalConstructor()
    ->disableOriginalClone()
    ->disableAutoload()
    ->getMock();

The chain should always start with the getMockBuilder () method and be downloaded with the getMock () method - these are the only links in the chain that are required.

Additional ways to get mock objects

  • getMockFromWsdl () - allows you to build Mock objects based on the description from WSDL ;
  • getMockClass () - creates a Mock class and returns its name as a string;
  • getMockForAbstractClass () - returns a Mock object of an abstract class in which all abstract methods are replaced.

All this is wonderful - you say, but what next? In response, I will say that we just came to the most interesting.

Waiting for a method call


PHPUnit allows us to control the number and order of calls to substituted methods. To do this, use the expects () construct, followed by the desired method using method (). As an example, we turn to the class at the beginning of the article and write this test for it:
public function test_process() {
    $mock = $this->getMock('MyClass', array('getTemperature', 'getWord', 'showWord'));
    $mock->expects($this->once())->method('getTemperature');
    $mock->expects($this->once())->method('showWord');
    $mock->expects($this->once())->method('getWord');
    $mock->process();
}

The result of this test will be successful if, when calling the process () method, a single call to the three listed methods occurs: getTemperature (), getWord (), showWord (). Please note that in the test, the call to getWord () is checked after the call to showWord () is checked, although the opposite is true in the test method. That's right, there is no error here. To control the order of method calls in PHPUnit, another construction is used - at (). Let's fix our test code a bit so that PHPUnit at the same time checks the sequence of method calls:
public function test_process() {
    $mock = $this->getMock('MyClass', array('getTemperature', 'getWord', 'showWord'));
    $mock->expects($this->at(0))->method('getTemperature');
    $mock->expects($this->at(2))->method('showWord');
    $mock->expects($this->at(1))->method('getWord');
    $mock->process();
}

In addition to the mentioned once () and at () for testing call expectations in PHPUnit, there are also the following constructs: any (), never (), atLeastOnce () and exactly ($ count). Their names speak for themselves.

Return result override


By far, the most useful feature of Mock objects is the ability to emulate the returned result by spoofed methods. We turn again to the process () method of our class. We see there an appeal to the temperature sensor - getTemperature (). But we also remember that in fact we do not have a sensor. Although even if we had it, we will not cool it below 15 degrees or heat above 25 in order to test all possible situations. As you may have guessed, in this case Mock objects come to our aid. We can make the method of interest to us return any result we want using will (). Here is an example:
/**
 * @dataProvider provider_process
 */
public function test_process($temperature) {
    $mock = $this->getMock('MyClass', array('getTemperature', 'getWord', 'showWord'));
    // метод getTemperature() вернет значение $temperature
    $mock->expects($this->once())->method('getTemperature')->will($this->returnValue($temperature));
    $mock->process();
}
public static function provider_process() {
    return array(
        'cold' => array(10),
        'warm' => array(20),
        'hot' => array(30),
    );
}

Obviously, this test covers all possible values ​​that our tested class can handle. PHPUnit offers the following constructs for use with will ():
  • returnValue ($ value) - returns $ value;
  • returnArgument ($ index) - returns the argument with the number $ index specified when the method was called;
  • returnSelf () - returns a pointer to itself, useful for testing chain methods;
  • returnValueMap ($ map) - used to return the result based on specific sets of arguments;
  • returnCallback ($ callback) - passes arguments to the specified function and returns its result;
  • onConsecutiveCalls () - sequentially returns one of the listed arguments with each subsequent method call;
  • throwException ($ exception) - throws the specified exception.

Validate Arguments


Another useful feature of Mock objects for testing is checking the arguments specified when invoking a spoofed method using the with () construct:
public function test_with_and_will_usage() {
    $mock = $this->getMock('MyClass', array('getWord'));
    $mock->expects($this->once())
        ->method('getWord')
        ->with($this->greaterThan(25))
        ->will($this->returnValue('hot'));
    $this->assertEquals('hot', $mock->getWord(30));
}

As arguments, with () can take all the same constructs as the assertThat () check, so here I will provide only a list of possible constructs without a detailed description:
  • attribute ()
  • anything ()
  • arrayHasKey ()
  • contains ()
  • equalTo ()
  • attributeEqualTo ()
  • fileExists ()
  • greaterThan ()
  • greaterThanOrEqual ()
  • classHasAttribute ()
  • classHasStaticAttribute ()
  • hasAttribute ()
  • identicalTo ()
  • isFalse ()
  • isInstanceOf ()
  • isNull ()
  • isTrue ()
  • isType ()
  • lessThan ()
  • lessThanOrEqual ()
  • matchesRegularExpression ()
  • stringContains ()

All of these constructs can be combined using the logical constructs logicalAnd (), logicalOr (), logicalNot (), and logicalXor ():
$mock->expects($this->once())
    ->method('getWord')
    ->with($this->logicalAnd($this->greaterThanOrEqual(15), $this->lessThanOrEqual(25)))
    ->will($this->returnValue('warm'));

Now that we have fully familiarized ourselves with the capabilities of Mock objects in PHPUnit, we can conduct the final testing of our class:
/**
 * @dataProvider provider_process
 */
public function test_process($temperature, $expected_word) {
    // получаем Mock объект, методы getWord() и process() наследуют логику от оригинального класса
    $mock = $this->getMock('MyClass', array('getTemperature', 'showWord'));
    // метод getTemperature() возвращает значение аргумента $temperature
    $mock->expects($this->once())->method('getTemperature')->will($this->returnValue($temperature));
    // проверяем, что метод showWord() запускается со значением $expected_word
    $mock->expects($this->once())->method('showWord')->with($this->equalTo($expected_word));
    // запуск
    $mock->process();
}
public static function provider_process() {
    return array(
        'cold' => array(10, 'cold'),
        'warm' => array(20, 'warm'),
        'hot' => array(30, 'hot'),
    );
}

UPD: VolCh rightly remarked that writing tests like the one presented above is antipattern. Therefore, the above example should be considered solely in order to familiarize yourself with the capabilities of Mock objects in PHPUnit.

Static Method Substitution


Starting with PHPUnit version 3.5, it is possible to replace static methods using the static construction of staticExpects ():
$class = $this->getMockClass('SomeClass');
// работает только в PHP версии 5.3 и выше
// в более ранних версиях можно использовать call_user_func_array()
$class::staticExpects($this->once())->method('someStaticMethod');
$class::someStaticMethod();

For this innovation to have practical application, it is necessary that inside the tested class the call of the replaceable static method should occur in one of the following ways:
  • $ this-> staticMethod () - from dynamic methods;
  • static :: staticMethod () - from static and dynamic methods.

Due to self restrictions, substitution of static methods called inside the class in this way will not work:
self::staticMethod();

Conclusion


In conclusion, I will say that you should not get too carried away with Mock objects for any purpose other than isolating the tested class from external data sources. Otherwise, with any, even minor, change in the source code, you will most likely need to edit the tests themselves as well. A fairly significant refactoring can lead to the fact that you have to completely rewrite them completely.

Read Next