📜 ⬆️ ⬇️

PHPUnit: Mock Objects

Quite often when writing unit tests, we have to deal with the fact that the class under test depends on data from external sources, the state of which we cannot control. Such sources include a far-located publicly accessible 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 in English is a parody), allowing us to test classes in isolation from external dependencies. This article is devoted to the use of Mock objects in PHPUnit.

As an example, 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 is inaccessible, and an attempt to access them may lead to a program crash.

In the simplest case, to check the logic, we can inherit from the specified class, replace the plugs with methods that apply to unconnected devices, and conduct unit testing on the child instance. Approximately the same implemented Mock objects in PHPUnit, where it provides additional convenience in the form of built-in API.

Getting a 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 the getMock () constructor as the second argument to a null value will result in a Mock object being returned without any changes at all.

getMockBuilder

For those who prefer to write in a chain style, PHPUnit offers the corresponding constructor:
 $mock = $this->getMockBuilder('MyClass') ->setMethods(null) ->setConstructorArgs(array()) ->setMockClassName('') //   ,   Mock  "" ->disableOriginalConstructor() ->disableOriginalClone() ->disableAutoload() ->getMock(); 

The chain should always begin with the getMockBuilder () method and be uploaded with the getMock () method — these are the only links in the chain that are mandatory.

Additional ways to get Mock objects


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

Waiting for method call


PHPUnit allows us to control the number and order of calls of the substituted methods. To do this, use the construction of expects () followed by the indication of the desired method using method (). As an example, let us turn to the class cited at the beginning of the article and write the following 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 of the three listed methods occurs: getTemperature (), getWord (), showWord (). Note that in the test, the test for the getWord () call is after the test for the showWord () call, although in the test method, the opposite is true. That's right, there is no error. To control the order of calling methods in PHPUnit, another construction is used - at (). Let's correct a little the code of our test so that PHPUnit checks the sequence of the method call at the same time:
 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 once () and at () mentioned, there are also the following constructs for testing call expectations in PHPUnit: any (), never (), atLeastOnce () and exactly ($ count). Their names speak for themselves.

Override return result


By far the most useful function of Mock objects is the ability to emulate the returned result by substituted methods. Let us again refer to the process () method of our class. We see there is 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 one, we would not cool it below 15 degrees or heat it 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 force the method that interests us to return any result we want using the will () construction. 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 test class can handle. PHPUnit offers the following constructions for use with will ():

Validation of the specified arguments


Another useful feature for testing the ability of Mock objects is to check the arguments specified when calling the substituted method using the with () construction:
 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 with arguments, it can take all the same constructions as the check of assertThat (), so here I will only give a list of possible constructions without their detailed description:

All the listed constructions can be combined using 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 are fully familiar with the capabilities of Mock objects in PHPUnit, we can conduct 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 noted 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.

Substitution of static methods


Starting with PHPUnit version 3.5, it became possible to substitute static methods using the static construct staticExpects ():
 $class = $this->getMockClass('SomeClass'); //    PHP  5.3   //       call_user_func_array() $class::staticExpects($this->once())->method('someStaticMethod'); $class::someStaticMethod(); 

In order for this innovation to be of practical use, it is necessary that within the class under test a call to the replaced static method be performed in one of the following ways:

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

Conclusion


In conclusion, I’ll say that you shouldn’t get too carried away with Mock objects for any purpose other than isolating the class under test from external data sources. Otherwise, for any, even minor, change of the source code, you most likely will also need to edit the tests themselves. A rather significant refactoring can lead to the fact that you have to completely rewrite them altogether.

Source: https://habr.com/ru/post/140217/


All Articles