Search code examples
phpunit-testingphpunitservice-locator

Testing and mocking result of a nested container


I recently took over a PHP project, which contains little to no tests. This project is fairly large, the classes are mostly quite small, but there is one big issue.

There is a service locator cleverly hidden inside either protected or private variable DI, so programmers think they are doing the right thing, which acts as a singleton and is passed pretty much to every single class. A class, which in return uses it to retrieve the dependencies.

Last thursday I created a new team of 3 people, whose new responsibility is to focus solely on testing and its automation, and today finally one of them came to me with the question I feared. A question, I don't know the answer to.

David, how am I suppose to mock the result of the method which is hidden deeply within the DI?

Rewriting the modules to follow DI is unacceptable, we have neither the budget nor the time to do that.

A do() method may be called like this?

class Baz extends AbstractBaz
{
    public function foo()
    {
        $userProcess = $this->DI->Foo->Bar->FooBar->BarFoo->getUserBar();
        $users = $userProcess->do();

        // work with the $users variable
    }
}

The locator itself is huge, you are able to call hundreds of methods by diving deeper and deeper into it.

Is there a way to quickly mock the Foo->Bar->FooBar->BarFoo->getUserBar result? The variables are available through the magic __get method and hinted by the @property annotation.

Using PHPUnit, it would be nice to have something like this:

$locator = $this
    ->getMockBuilder('\App\DI\Abstracted\DI')
    ->setMethods(['Foo->Bar->FooBar->BarFoo->getUserBar'])
    ->getMock();

$locator
    ->expects($this->any())
    ->method('Foo->Bar->FooBar->BarFoo->getUserBar')
    ->will($this->returnValue($desiredObject));

Sadly, that does not really work. I am not very skilled with PHPUnit myself. Is there a workaround I haven't found yet?


Solution

  • I found the solution for PHPUnit earlier, but only just happened to have the time to answer.

    Simulating what I wanted is actually possible in PHPUnit, using PHP's Closure, which is what anonymous functions in PHP are called.

    Here is a little working PHPUnit example

    $classA = $this
        ->getMockBuilder('First\Class\To\Be\Mocked')
        ->disableOriginalConstructor()
        ->setMethods([
            'The',
            'names',
            'of',
            'desired',
            'methods',
        ])
        ->getMock();
    
    $classB = $this
        ->getMockBuilder('Second\Class\To\Be\Mocked')
        ->disableOriginalConstructor()
        ->setMethods([
            'The',
            'names',
            'of',
            'desired',
            'methods',
        ])
        ->getMock();
    
    $serviceLocator = $this
        ->getMockBuilder('Second\Class\To\Be\Mocked')
        ->disableOriginalConstructor()
        ->setMethods([
            '__get',
            'SomeMethod',
        ])
        ->getMock();
    
    $serviceLocator
        ->expects($this->any())
        ->method('__get')
        ->will($this->returnCallback(function($name) use ($serviceLocator, $classA, $classB)
        {
            switch ($name)
            {
                case 'ClassA':
                    return $classA;
                case 'ClassB':
                    return $classB;
                default:
                    return $serviceLocator;
            }
        }));
    

    This code when executed will give the desired returns.

    $serviceLocator->This->That->Them->ClassA would return the defined $classA variable, changing ClassA to ClassB makes the service locator return the $classB variable instead.

    You can do pretty much anything using callbacks, even simulate return values through reference parameters.