PHPUnit – Testing Interface Injection Mocks

I hate PHPUnit… ok, maybe not hate, but I don’t like writing more code to test the code I originally wrote.  Plus when the amount of code required for the test is usually twice as much, that just seems a bit inefficient.

Digressing a bit, there are amazing benefits to testing your code! By simply writing unit tests you will ensure that you can catch errors early, refactoring becomes easier, and much more.  I like to apply a bit of Grey Box Testing methodology to Unit Testing.  I think that testing too much of the internal workings of the function or application will result in a full refactor of the unit tests when the code is refactored.  Thus rendering the unit tests useless.

The best way to ensure that you can meet your code standards, catch problems early, and be agnostic to refactoring, is by testing the input and output of the methods.  You will obviously try to test each possible outcome, so you’ll have to know the  internal working of each method a bit more.

Zend Framework 2, among others, implements a dependency injection system.  While this is very beneficial for writing code,  it’s great for testing as well, as long as your tests don’t have to inject every single dependency each time.  That would be too time consuming and likely a pain in the ass as well.  When things are too tedious, or difficult, most developers will tend to make it a “ToDo” and move on.  With ZF2, there’s an easy setup for interface dependency injection.  We can apply this same configuration to PHPUnit:

Gist url here

<?php
use PHPUnit_Framework_TestCase as TestCase,
    PHPUnit_Framework_MockObject_MockBuilder as MockBuilder,
    PHPUnit_Framework_MockObject_MockObject as MockObject;

trait ZF2InterfaceInjectionTrait extends TestCase
{

    /**
     * Provides the default mocks for each of the interfaces defined in your test configuration
     * @param  String|Object $mock if provided a string, I will create the mock for you, if not, I will apply the mocks to the object provided.
     * @param  array  $args
     * @return Object
     */
    protected function interfaceAwareMock($mock, array $args = array())
    {
        $config    = InterfaceAwareMockBuilder::getConfig();
        $instance  = is_string($mock) ? $this->getMock($mock, $args) : $mock;
        if (isset($config['dependencies'])) {
            foreach ($config['dependencies']['interfaces'] as $interface => $invoke) {
                if ($instance instanceof $interface) {
                    $instance->$invoke['method']($this->getMockBuilder($invoke['invokable'])->disableOriginalConstructor()->getMock());
                }
            }
        }
        return $instance;
    }

    /**
     * Provides the default mocks for each of the interfaces defined in the test configuration
     * @param  String|Object $mock If this method is provided a string, it will create the mock, if not, it will apply the arguments to the provided mock.
     * @param  array  $args
     * @return Object
     */
    protected function interfaceAware($mock, array $args = array())
    {
        $instance  = is_string($mock) ? $this->getInterfaceAwareMockBuilder($mock)->setMethods($args)->disableOriginalConstructor() : $mock;
        return $instance;
    }    

    /**
     * Returns a builder object to create mock objects using a fluent interface.
     *
     * @param  string $className
     * @return PHPUnit_Framework_MockObject_MockBuilder
     * @since  Method available since Release 3.5.0
     */
    public function getInterfaceAwareMockBuilder($className)
    {
        return new InterfaceAwareMockBuilder(
          $this, $className
        );
    }

    /**
     * Shorthand use for simple mock writing!
     * example use:
     *    $this->testObj = $this->interfaceAware('MyServiceMyClass', array('__construct'));
     *    $this->mock    = $this->getMockBuilder('MyServiceMyClassOtherMock')->disableOriginalConstructor();

     *    $mock          = $this->mock->getMock();
     *
     *    // Interface aware mocks will support the getMockSetup as a chain
     *    $localTest = $this->testObj->getMockSetup(array(
     *        'methodIAmOverwriting' => array(
     *            'expects' => $this->once(),
     *            'will' => $this->returnValue($mock)
     *        ),
     *    ));

     *    // Simple use case
     *    $this->getMockSetup($this->mock, array(
     *        'save' => array(
     *            'expects' => $this->exactly(1),
     *            'with' => $otherMock
     *        ),
     *    ));
     *
     * @param PHPUnit_Framework_MockObject_MockObject $mock
     * @param array $methods
     */
    public function getMockSetup($mock, array $methods = array())
    {
        if ($mock instanceof MockBuilder) {
            $mock = $mock->setMethods(array_keys($methods))->getMock();            
        }
        foreach ($methods as $method => $definition){
            $chain = $mock->expects($definition['expects'])->method($method);
            unset($definition['expects']);
            foreach ($definition as $key => $value){
                if (is_array($value)) {
                    call_user_func_array(array($chain, $key), $value);
                } else {
                    call_user_func(array($chain, $key), $value);
                }
            }
        }
        return $mock;
    }
}

class InterfaceAwareMockBuilder extends MockBuilder
{
    public static function getConfig()
    {
        return array(
            'dependencies' => array(
                'interfaces' => array(
                    'SomeServiceServiceAwareInterface' => array(
                        'setMethod' => 'setService', 
                        'invokable' => 'SomeService'
                    ),
                ),
            ),
        );
    }

    public function getMock()
    {
        $instance  = parent::getMock();
        $config    = self::getConfig();
        if (isset($config['dependencies'])) {
            foreach ($config['dependencies']['interfaces'] as $interface => $invoke) {
                if ($instance instanceof $interface) {
                    $mock = $this->testCase->getMockBuilder($invoke['invokable'])->disableOriginalConstructor()->getMock();
                    $instance->$invoke['setMethod']($mock);
                }
            }
        }
        return $instance;
    }

    public function getMockSetup(array $methods = array())
    {
        return $this->testCase->getMockSetup($this, $methods);
    }
}

 

Leave a comment