jueves, 22 de noviembre de 2012

Mockery: A Better Way

Mockery is a PHP extension that offers a superior mocking experience, particularly when compared to PHPUnit. While PHPUnit’s mocking framework is powerful, Mockery offers a more natural language with a Hamcrest-like set of matchers. In this article, I’ll compare the two mocking frameworks and highlight the best features of Mockery.

Mockery offers a set of mocking-related matchers that are very similar to a Hamcrest dictionary, offering a very natural way to express mocked expectations. Mockery does not override or conflict with PHPUnit’s built-in mocking functions; in fact, you can use them both at the same time (and even in the same test method).


Installing Mockery

There are multiple ways to install Mockery; here are the most common methods.

Use Composer

Create a file called composer.json in you project’s root folder, and add the following code to that file:

  {      "require": {          "Mockery/Mockery": ">=0.7.2"      }  }  

Next, simply install Composer in your project’s root folder using the following command:

  curl -s http://getcomposer.org/installer | php  

Finally, install any required dependencies (including Mockery) with this command:

  php composer.phar install  

With everything installed, let’s ensure that our Mockery installation works. For the sake of simplicity, I’ll assume that you have a folder, called Test in your project’s root directory. All of the examples in this tutorial will reside in this folder. Here’s the code I’ve used to ensure that Mockery works with my project:

  //Filename: JustToCheckMockeryTest.php  require_once '../vendor/autoload.php';  class JustToCheckMockeryTest extends PHPUnit_Framework_TestCase {          protected function tearDown() {                  \Mockery::close();          }          function testMockeryWorks() {                  $mock = \Mockery::mock('AClassToBeMocked');                  $mock->shouldReceive('someMethod')->once();                  $workerObject = new AClassToWorkWith;                  $workerObject->doSomethingWit($mock);          }  }  class AClassToBeMocked {}  class AClassToWorkWith {          function doSomethingWit($anotherClass) {                  return $anotherClass->someMethod();          }  }  

Linux Users: Use Your Distro’s Packages

Some Linux distributions make it easy to install Mockery, but only a handful provide a Mockery package for their system. The following list are the only distros I’m aware of:

  • Sabayon: equo install Mockery
  • Fedora / RHE: yum install Mockery

Use PEAR

PEAR fans can install Mockery by issuing the following commands:

  sudo pear channel-discover pear.survivethedeepend.com  sudo pear channel-discover hamcrest.googlecode.com/svn/pear  sudo pear install --alldeps deepend/Mockery  

Installing from Source

Installing from GitHub is for the real geeks! You can always grab the latest version of Mockery through its GitHub repository.

  git clone git://github.com/padraic/Mockery.git  cd Mockery  sudo pear channel-discover hamcrest.googlecode.com/svn/pear  sudo pear install --alldeps package.xml  

Creating our First Mocked Object

Let’s mock some objects before we define any expectations. The following code will modify the previous example to include both PHPUnit and Mockery examples:

  //Filename: MockeryABetterWayOfMockingTest.php  require_once '../vendor/autoload.php';  class MockeryVersusPHPUnitGetMockTest extends PHPUnit_Framework_TestCase {          function testCreateAMockedObject() {                  // With PHPUnit                  $phpunitMock = $this->getMock('AClassToBeMocked');                  // With Mockery                  $mockeryMock = \Mockery::mock('AClassToBeMocked');          }  }  class AClassToBeMocked {  }  

Mockery allows you to define mocks for classes that do not exist.

The first line ensures that we have access to Mockery. Next, we create a test class, called MockeryVersusPHPUnitGetMockTest, which has a method, testCreateAMockedObject(). The mocked class, AClassToBeMocked, is completely empty at this time; in fact, you could completely remove the class without causing the test to fail.

The testCreateAMockedObject() test method defines two objects. The first is a PHPUnit mock, and the second is created with Mockery. Mockery’s syntax is:

  $mockedObject = \Mockery::mock('SomeClassToBeMocked');  

Assign Simple Expectations

Mocks are commonly used to verify an object’s behavior (primarily its methods) by specifying what are called expectations. Let’s set up a few simple expectations.

Expect a Method to be Called

Probably the most common expectation is one that expects a specific method call. Most mocking frameworks allow you to specify the amount of calls you expect a method to receive. We’ll begin with a simple single call expectation:

  //Filename: MockeryABetterWayOfMockingTest.php  require_once '../vendor/autoload.php';  class MockeryVersusPHPUnitGetMockTest extends PHPUnit_Framework_TestCase {          protected function tearDown() {                  \Mockery::close();          }          function testExpectOnce() {                  $someObject = new SomeClass();                  // With PHPUnit                  $phpunitMock = $this->getMock('AClassToBeMocked');                  $phpunitMock->expects($this->once())->method('someMethod');                  // Exercise for PHPUnit                  $someObject->doSomething($phpunitMock);                  // With Mockery                  $mockeryMock = \Mockery::mock('AnInexistentClass');                  $mockeryMock->shouldReceive('someMethod')->once();                  // Exercise for Mockery                  $someObject->doSomething($mockeryMock);          }  }  class AClassToBeMocked {          function someMethod() {}  }  class SomeClass {          function doSomething($anotherObject) {                  $anotherObject->someMethod();          }  }  

This code configures an expectation for both PHPUnit and Mockery. Let’s start with the former.

Some Linux distributions make it easy to install Mockery.

We use the expects() method to define an expectation to call someMethod() once. But in order for PHPUnit to work correctly, we must define a class called AClassToBeMocked, and it must have a someMethod() method.

This is a problem. If you are mocking a lot of objects and developing using TDD principles for a top-down design, you would not want to create all the classes and methods before your test. Your test should fail for the right reason, that the expected method was not called, instead of a critical PHP error with no relation to the real implementation. Go ahead and try to remove the someMethod() definition from AClassToBeMocked, and see what happens.

Mockery, on the other hand, allows you to define mocks for classes that do not exist.

Notice that the above example creates a mock for AnInexistentClass, which as its name implies, does not exist (nor does its someMethod() method).

At the end of the above example, we define the SomeClass class to exercise our code. We initialize an object, called $someObject in the first line of the test method, and we effectively exercise the code after defining our expectations.

Please Note: Mockery evaluates expectations on its close() method. For this reason, you should always have a tearDown() method on your test that calls \Mockery::close(). Otherwise, Mockery gives false positives.

Expect More Than One Call

As I noted previously, most mocking frameworks have the ability to specify expectations for multiple method calls. PHPUnit uses the $this->exactly() construct for this purpose. The following code defines the expectations for calling a method multiple times:

  function testExpectMultiple() {          $someObject = new SomeClass();          // With PHPUnit 2 times          $phpunitMock = $this->getMock('AClassToBeMocked');          $phpunitMock->expects($this->exactly(2))->method('someMethod');          // Exercise for PHPUnit          $someObject->doSomething($phpunitMock);          $someObject->doSomething($phpunitMock);          // With Mockery 2 times          $mockeryMock = \Mockery::mock('AnInexistentClass');          $mockeryMock->shouldReceive('someMethod')->twice();          // Exercise for Mockery          $someObject->doSomething($mockeryMock);          $someObject->doSomething($mockeryMock);          // With Mockery 3 times          $mockeryMock = \Mockery::mock('AnInexistentClass');          $mockeryMock->shouldReceive('someMethod')->times(3);          // Exercise for Mockery          $someObject->doSomething($mockeryMock);          $someObject->doSomething($mockeryMock);          $someObject->doSomething($mockeryMock);  }  

Mockery provides two different methods to better suit your needs. The first method, twice(), expects two method calls. The other method is times(), which lets you specify an amount. Mockery’s approach is much cleaner and easier to read.


Returning Values

Another common use for mocks is to test a method’s return value. Naturally, both PHPUnit and Mockery have the means to verify return values. Once again, let’s start with something simple.

Simple Return Values

The following code contains both PHPUnit and Mockery code. I also updated SomeClass to provide a testable return value.

  class MockeryVersusPHPUnitGetMockTest extends PHPUnit_Framework_TestCase {          protected function tearDown() {                  \Mockery::close();          }          // [...] //          function testSimpleReturnValue() {                  $someObject = new SomeClass();                  $someValue = 'some value';                  // With PHPUnit                  $phpunitMock = $this->getMock('AClassToBeMocked');                  $phpunitMock->expects($this->once())->method('someMethod')->will($this->returnValue($someValue));                  // Expect the returned value                  $this->assertEquals($someValue, $someObject->doSomething($phpunitMock));                  // With Mockery                  $mockeryMock = \Mockery::mock('AnInexistentClass');                  $mockeryMock->shouldReceive('someMethod')->once()->andReturn($someValue);                  // Expect the returned value                  $this->assertEquals($someValue, $someObject->doSomething($mockeryMock));          }  }  class AClassToBeMocked {          function someMethod() {          }  }  class SomeClass {          function doSomething($anotherObject) {                  return $anotherObject->someMethod();          }  }  

Both PHPUnit’s and Mockery’s API is straight-forward and easy to use, but I still find Mockery to be cleaner and more readable.

Returning Different Values

Frequent unit testers can testify to complications with methods that return different values. Unfortunately, PHPUnit’s limited $this->at($index) method is the only way to return different values from the same method. The following code demonstrates the at() method:

  function testDemonstratePHPUnitCallIndexing() {          $someObject = new SomeClass();          $firstValue = 'first value';          $secondValue = 'second value';          // With PHPUnit          $phpunitMock = $this->getMock('AClassToBeMocked');          $phpunitMock->expects($this->at(0))->method('someMethod')->will($this->returnValue($firstValue));          $phpunitMock->expects($this->at(1))->method('someMethod')->will($this->returnValue($secondValue));          // Expect the returned value          $this->assertEquals($firstValue, $someObject->doSomething($phpunitMock));          $this->assertEquals($secondValue, $someObject->doSomething($phpunitMock));  }  

This code defines two separate expectations and makes two different calls to someMethod(); so, this test passes. But let’s introduce a twist and add a double call in the tested class:

  // [...] //  function testDemonstratePHPUnitCallIndexingOnTheSameClass() {          $someObject = new SomeClass();          $firstValue = 'first value';          $secondValue = 'second value';          // With PHPUnit          $phpunitMock = $this->getMock('AClassToBeMocked');          $phpunitMock->expects($this->at(0))->method('someMethod')->will($this->returnValue($firstValue));          $phpunitMock->expects($this->at(1))->method('someMethod')->will($this->returnValue($secondValue));          // Expect the returned value          $this->assertEquals('first value second value', $someObject->concatenate($phpunitMock));  }  class SomeClass {          function doSomething($anotherObject) {                  return $anotherObject->someMethod();          }          function concatenate($anotherObject) {                  return $anotherObject->someMethod() . ' ' . $anotherObject->someMethod();          }  }  

The test still passes. PHPUnit expects two calls to someMethod() that happen inside the tested class when performing the concatenation via the concatenate() method. The first call returns the first value, and the second call returns the second value. But, here’s the catch: what would happen if you double the assertion? Here’s the code:

  function testDemonstratePHPUnitCallIndexingOnTheSameClass() {          $someObject = new SomeClass();          $firstValue = 'first value';          $secondValue = 'second value';          // With PHPUnit          $phpunitMock = $this->getMock('AClassToBeMocked');          $phpunitMock->expects($this->at(0))->method('someMethod')->will($this->returnValue($firstValue));          $phpunitMock->expects($this->at(1))->method('someMethod')->will($this->returnValue($secondValue));          // Expect the returned value          $this->assertEquals('first value second value', $someObject->concatenate($phpunitMock));          $this->assertEquals('first value second value', $someObject->concatenate($phpunitMock));  }  

It returns the following error:

  Failed asserting that two strings are equal.  --- Expected  +++ Actual  @@ @@  -'first value second value'  +' '  

PHPUnit continues counting between distinct calls to concatenate(). By the time the second call in the last assertion occurs, $index is at the values 2 and 3. You can make the test pass by modifying your expectations to consider the two new steps, like this:

  function testDemonstratePHPUnitCallIndexingOnTheSameClass() {          $someObject = new SomeClass();          $firstValue = 'first value';          $secondValue = 'second value';          // With PHPUnit          $phpunitMock = $this->getMock('AClassToBeMocked');          $phpunitMock->expects($this->at(0))->method('someMethod')->will($this->returnValue($firstValue));          $phpunitMock->expects($this->at(1))->method('someMethod')->will($this->returnValue($secondValue));          $phpunitMock->expects($this->at(2))->method('someMethod')->will($this->returnValue($firstValue));          $phpunitMock->expects($this->at(3))->method('someMethod')->will($this->returnValue($secondValue));          // Expect the returned value          $this->assertEquals('first value second value', $someObject->concatenate($phpunitMock));          $this->assertEquals('first value second value', $someObject->concatenate($phpunitMock));  }  

You can probably live with this code, but Mockery makes this scenario trivial. Don’t believe me? Take a look:

  function testMultipleReturnValuesWithMockery() {          $someObject = new SomeClass();          $firstValue = 'first value';          $secondValue = 'second value';          // With Mockery          $mockeryMock = \Mockery::mock('AnInexistentClass');          $mockeryMock->shouldReceive('someMethod')->andReturn($firstValue, $secondValue, $firstValue, $secondValue);          // Expect the returned value          $this->assertEquals('first value second value', $someObject->concatenate($mockeryMock));          $this->assertEquals('first value second value', $someObject->concatenate($mockeryMock));  }  

Like PHPUnit, Mockery uses index counting, but we don’t have to worry about indices. Instead, we simply list all the expected values, and Mockery returns them in order.

Additionally, PHPUnit returns NULL for unspecified indexes, but Mockery always returns the last specified value. That’s a nice touch.

Try Multiple Methods with Indexing

Let’s introduce a second method into our code, the concatWithMinus() method:

  class SomeClass {          function doSomething($anotherObject) {                  return $anotherObject->someMethod();          }          function concatenate($anotherObject) {                  return $anotherObject->someMethod() . ' ' . $anotherObject->someMethod();          }          function concatWithMinus($anotherObject) {                  return $anotherObject->anotherMethod() . ' - ' . $anotherObject->anotherMethod();          }  }  

This method behaves similarly to concatenate(), but it concatenates the string values with “ - ” as opposed to a single space. Because these two methods perform similar tasks, it makes sense to to test them inside the same test method to avoid duplicate testing.

As demonstrated in the above code, the second function uses a different mocked method called anotherMethod(). I made this change to force us to mock both methods in our tests. Our mockable class now looks like this:

  class AClassToBeMocked {          function someMethod() {          }          function anotherMethod() {          }  }  

Testing this with PHPUnit might look like the following:

  function testPHPUnitIndexingOnMultipleMethods() {          $someObject = new SomeClass();          $firstValue = 'first value';          $secondValue = 'second value';          // With PHPUnit          $phpunitMock = $this->getMock('AClassToBeMocked');          // First and second call on the semeMethod:          $phpunitMock->expects($this->at(0))->method('someMethod')->will($this->returnValue($firstValue));          $phpunitMock->expects($this->at(1))->method('someMethod')->will($this->returnValue($secondValue));          // Expect the returned value          $this->assertEquals('first value second value', $someObject->concatenate($phpunitMock));          // First and second call on the anotherMethod:          $phpunitMock->expects($this->at(0))->method('anotherMethod')->will($this->returnValue($firstValue));          $phpunitMock->expects($this->at(1))->method('anotherMethod')->will($this->returnValue($secondValue));          // Expect the returned value          $this->assertEquals('first value - second value', $someObject->concatWithMinus($phpunitMock));  }  

The logic is sound. Define two different expectations for each method and specify the return value. This works only with PHPUnit 3.6 or newer.

Please Note: PHPunit 3.5 and older had a bug which did not reset the index for each method, resulting in unexpected return values for mocked methods.

Let’s look at the same scenario with Mockery. Once again, we get much cleaner code. See for yourself:

  function testMultipleReturnValuesForDifferentFunctionsWithMockery() {          $someObject = new SomeClass();          $firstValue = 'first value';          $secondValue = 'second value';          // With Mockery          $mockeryMock = \Mockery::mock('AnInexistentClass');          $mockeryMock->shouldReceive('someMethod')->andReturn($firstValue, $secondValue);          $mockeryMock->shouldReceive('anotherMethod')->andReturn($firstValue, $secondValue);          // Expect the returned value          $this->assertEquals('first value second value', $someObject->concatenate($mockeryMock));          $this->assertEquals('first value - second value', $someObject->concatWithMinus($mockeryMock));  }  

Return Values Based on Given Parameter

Honestly, this is something PHPUnit simply cannot do. At the time of this writing, PHPUnit does not permit you to return different values from the same function based on the function’s parameter. Therefore, the following test fails:

  // [...] //  function testPHUnitCandDecideByParameter() {          $someObject = new SomeClass();          // With PHPUnit          $phpunitMock = $this->getMock('AClassToBeMocked');          $phpunitMock->expects($this->any())->method('getNumber')->with(2)->will($this->returnValue(2));          $phpunitMock->expects($this->any())->method('getNumber')->with(3)->will($this->returnValue(3));          $this->assertEquals(4, $someObject->doubleNumber($phpunitMock, 2));          $this->assertEquals(6, $someObject->doubleNumber($phpunitMock, 3));  }  class AClassToBeMocked {  // [...] //          function getNumber($number) {                  return $number;          }  }  class SomeClass {          // [...] //          function doubleNumber($anotherObject, $number) {                  return $anotherObject->getNumber($number) * 2;          }  }  

Please ignore the fact that there is no logic in this example; it would fail even if it was present. This code, however, does help illustrate the idea.

This test fails because PHPUnit cannot differentiate between the two expectations in the test. The second expectation, expecting parameter 3, simply overrides the first expecting parameter 2. If you attempt to run this test, you get the following error:

  Expectation failed for method name is equal to <string:getNumber> when invoked zero or more times  Parameter 0 for invocation AClassToBeMocked::getNumber(2) does not match expected value.  Failed asserting that 2 matches expected 3.  

Mockery can do this, and the code below works exactly as you would expect it to work. The method returns different values based on its provided parameters:

  function testMockeryReturningDifferentValuesBasedOnParameter() {          $someObject = new SomeClass();          // Mockery          $mockeryMock = \Mockery::mock('AnInexistentClass');          $mockeryMock->shouldReceive('getNumber')->with(2)->andReturn(2);          $mockeryMock->shouldReceive('getNumber')->with(3)->andReturn(3);          $this->assertEquals(4, $someObject->doubleNumber($mockeryMock, 2));          $this->assertEquals(6, $someObject->doubleNumber($mockeryMock, 3));  }  

Partial Mocks

Sometimes, you want to mock only specific methods on your object (as opposed to mocking an entire object). The following Calculator class already exists; we want to only mock certain methods:

  class Calculator {          function add($firstNo, $secondNo) {                  return $firstNo + $secondNo;          }          function subtract($firstNo, $secondNo) {                  return $firstNo - $secondNo;          }          function multiply($value, $multiplier) {                  $newValue = 0;                  for($i=0;$i<$multiplier;$i++)                          $newValue = $this->add($newValue, $value);                  return $newValue;          }  }  

This Calculator class has three methods: add(), subtract(), and multiply(). Multiply uses a loop to perform the multiplication by calling the add() for a specified amount of times (e.g. 2 x 3 is really 2 + 2 + 2).

Let’s assume that we want to test multiply() in total isolation; so, we’ll mock add() and check for specific behavior on multiply(). Here are some possible tests:

  function testPartialMocking() {          $value = 3;          $multiplier = 2;          $result = 6;          // PHPUnit          $phpMock = $this->getMock('Calculator', array('add'));          $phpMock->expects($this->exactly(2))->method('add')->will($this->returnValue($result));          $this->assertEquals($result, $phpMock->multiply($value,$multiplier));          // Mockery          $mockeryMock = \Mockery::mock(new Calculator);          $mockeryMock->shouldReceive('add')->andReturn($result);          $this->assertEquals($result, $mockeryMock->multiply($value,$multiplier));          // Mockery extended test checking parameters          $mockeryMock2 = \Mockery::mock(new Calculator);          $mockeryMock2->shouldReceive('add')->with(0,3)->andReturn(3);          $mockeryMock2->shouldReceive('add')->with(3,3)->andReturn(6);          $this->assertEquals($result, $mockeryMock2->multiply($value,$multiplier));  }  

Mockery offers…a very natural way to express mocked expectations.

The first PHPUnit test is anemic; it simply tests that the method add() is called twice and it returns the final value on each call. It gets the job done, but it’s also a little complicated. PHPUnit forces you to pass the list of methods that you want to mock as second parameter to $this->getMock(). Otherwise, PHPUnit would mock all methods, each returning NULL by default. This list must be kept in concordance with the expectations you define on your mocked object.

For example, if I add a second expectation to $phpMock‘s substract() method, PHPUnit would ignore it and call the original substract() method. That is, unless I explicitly specify the name of the method (substract) in the $this->getmock() statement.

Of course, Mockery is different by allowing you to provide a real object to \Mockery::mock(), and it automatically creates a partial mock. It achieves this by implementing a proxy-like solution for mocking. All the expectations you define are used, but Mockery falls back to the original method if you do not specify an expectation for that method.

Please Note: Mockery’s approach is very simple, but internal method calls do not pass through the mocked object.

This example is misleading, but it illustrates how not to use Mockery’s partial mocks. Yes, Mockery creates a partial mock if you pass a real object, but it only mocks only external calls. For example, based on the previous code, the multiply() method calls the real add() method. Go ahead and try to change the last expectation from ...->andReturn(6) to ...->andReturn(7). The test should obviously fail, but it doesn’t because the real add() executes instead of the mocked add() method.

But we can circumvent this issue by creating mocks like this:

  //Instead of  $mockeryMock = \Mockery::mock(new Calculator);  // Create the mock like this  $mockeryMock = \Mockery::mock('Calculator[add]');  

While syntactically different, the concept is similar to PHPUnit’s approach: you have to list the mocked methods in two places. But for any other test, you can just simply pass the real object, which is much easier–especially when dealing with constructor parameters.


Dealing with Constructor Parameters

Let’s add a constructor with two parameters to the Calculator class. The revised code:

  class Calculator {          public $myNumbers = array();          function __construct($firstNo, $secondNo) {                  $this->myNumbers[]=$firstNo;                  $this->myNumbers[]=$secondNo;          }          // [...] //  }  

Every test in this article will fail after adding this constructor. More precisely, the testPartialMock() test results in the following error:

  Missing argument 1 for Calculator::__construct(),  called in /usr/share/php/PHPUnit/Framework/MockObject/Generator.php  on line 224 and defined  

PHPUnit tries to mock the real object by automatically calling the constructor, expecting to have the parameters correctly set. There are two ways around this problem: either set the parameters, or don’t call the constructor.

  //Specify Constructor Parameters  $phpMock = $this->getMock('Calculator', array('add'), array(1,2));  //Do not call original constructor  $phpMock = $this->getMock('Calculator', array('add'), array(), '', false);  

Mockery automagically works around this problem. It’s okay not to specify a constructor parameter; Mockery will simply not call the constructor. But you can specify a list of constructor parameters for Mockery to use. For example:

  function testMockeryConstructorParameters() {          $result = 6;          // Mockery          // Do not call constructor          $noConstrucCall = \Mockery::mock('Calculator[add]');          $noConstrucCall->shouldReceive('add')->andReturn($result);          // Use constructor parameters          $withConstructParams = \Mockery::mock('Calculator[add]', array(1,2));          $withConstructParams->shouldReceive('add')->andReturn($result);          // User real object with real values and mock over it          $realCalculator = new Calculator(1,2);          $mockRealObj = \Mockery::mock($realCalculator);          $mockRealObj->shouldReceive('add')->andReturn($result);  }  

Technical Considerations

Mockery is another library that integrates your tests, and you may want to consider what technical implications this may have.

  • Mockery uses a lot of memory. You will have to increase the maximum memory to 512MB if you want to run many tests(say over 1000 tests with more than 3000 assertions). See php.ini documentation for further details.
  • You have to organize your tests to run in separate processes, when mocking static methods and static method calls.
  • You can auto-load Mockery into every test by using PHPUnit’s bootstrap functionality (helpful when you have many tests and you don’t want to repeat yourself).
  • You can automate the call to \Mockery::close() in each test’s tearDown() by editing phpunit.xml.

Final Conclusions

PHPUnit certainly has its issues, especially when it comes to functionality and expressiveness. Mockery can greatly improve your mocking experience by making your tests easy to write and understand – but it’s not perfect (there’s no such thing!).

This tutorial has highlighted many key aspects of Mockery, but, honestly, we’ve barely scratched the surface. Be sure to explore the project’s Github repository to learn more.

Thanks for reading!



No hay comentarios:

Publicar un comentario