PHP - Design Patterns - Mock

php-design-patterns

https://code.tutsplus.com/tutorials/mockery-a-better-way--net-28097 - not done reading this yet

What is the purpose of Mock?

Mocks are commonly used to verify an object's behavior (primarily its methods) by specifying what are called expectations. Probably the most common expectation is one that expects a specific method call.

What is Mockery?

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).

How can we confirm that Mockery is installed successfully?

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

}

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:

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. 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.

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.

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.

class sgContactEvent_DispatcherMock extends sgContactEvent_Dispatcher
{
    /**
     * Clears out all registered observers for all channels. This should ONLY be
     * used for testing.
     */
    public static function clearRegisteredObservers()
    {
        foreach(self::$channelObservers as $channel => $observerList)
        {
            self::$channelObservers[$channel] = array();
        }
    }

}

How can we expect more than one call in our test?

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.

How can we use mocks to test a method's return value?

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.

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.

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

}
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License