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