Seems our technical architect is becoming my PHP muse. Good on ya, Pete.
Anyway, he just sent out what I thought was a controversial email in which he said (paraphrase) "we're calling setMethods way too often: in general we don't need to call it". To contextualise, I mean this:
$dependency = $this
->getMockBuilder(MyDependency::class)
->setMethods(['someMethod'])
->getMock();
$dependency
->method('someMethod')
->willReturn('MOCKED RESULT');
See here I'm explicitly saying "mock out the someMethod method". Pete's contention is that that's generally unnecessary, cos getMockBuilder mocks 'em all by default anyhow. This goes against my understanding of how it works, and I've written bloody thousands of unit tests with PHPUnit so it surprised me I was getting it wrong. I kinda believed him cos he said he tested it, but I didn't believe him so much as to not go test it myself.
So I knocked out a quick service and dependency for it:
namespace me\adamcameron\myApp;
class MyService
{
private $myDecorator;
public function __construct(MyDecorator $myDecorator)
{
$this->myDecorator = $myDecorator;
}
public function decorateMessage($message)
{
$message = $this->myDecorator->addPrefix($message);
$message = $this->myDecorator->addSuffix($message);
return $message;
}
}
namespace me\adamcameron\myApp;
class MyDecorator
{
public function addPrefix($message)
{
return "(ACTUAL PREFIX) $message";
}
public function addSuffix($message)
{
return "$message (ACTUAL SUFFIX)";
}
}
Nothing surprising there: the service takes that decorator as a constructor arg, and then when calling decorateMessage the decorator's methods are called.
In our tests of decorateMessage, we don't want to use the real decorator methods, we want to use mocks.
First I have a test the way I'd normally mock things: explicitly calling setMethods with the method names:
public function testWithExplicitMock()
{
$decorator = $this
->getMockBuilder(MyDecorator::class)
->setMethods(['addPrefix', 'addSuffix'])
->getMock();
$decorator
->method('addPrefix')
->willReturn('(MOCKED PREFIX)');
$decorator
->method('addSuffix')
->willReturn('(MOCKED SUFFIX)');
$service = new MyService($decorator);
$result = $service->decorateMessage('TEST MESSAGE');
$this->assertSame(
'(MOCKED SUFFIX)',
$result
);
}
And this mocks out the method correctly. This is my baseline.
Secondly, I still call setMethods, but I give it an empty array:
public function testWithImplicitMock()
{
$decorator = $this
->getMockBuilder(MyDecorator::class)
->setMethods([])
->getMock();
$decorator
->method('addPrefix')
->willReturn('(MOCKED PREFIX)');
$decorator
->method('addSuffix')
->willReturn('(MOCKED SUFFIX)');
$service = new MyService($decorator);
$result = $service->decorateMessage('TEST MESSAGE');
$this->assertSame(
'(MOCKED SUFFIX)',
$result
);
}
This also mocks out the method correctly. Passing an empty array mocks out all the methods in the mocked class.
Next I try passing null to setMethods:
public function testWithNull()
{
$decorator = $this
->getMockBuilder(MyDecorator::class)
->setMethods(null)
->getMock();
$decorator
->method('addPrefix')
->willReturn('(MOCKED PREFIX)');
$decorator
->method('addSuffix')
->willReturn('(MOCKED SUFFIX)');
$service = new MyService($decorator);
$result = $service->decorateMessage('TEST MESSAGE');
$this->assertSame(
'(ACTUAL PREFIX) TEST MESSAGE (ACTUAL SUFFIX)',
$result
);
}
This creates the mocked object, but the methods are not mocked. So the result here is using the actual methods.
In the last example I mock one of the methods (addPrefix), but not the other (addSuffix):
public function testWithOneExplicitMock()
{
$decorator = $this
->getMockBuilder(MyDecorator::class)
->setMethods(['addPrefix'])
->getMock();
$decorator
->method('addPrefix')
->willReturn('(MOCKED PREFIX)');
$service = new MyService($decorator);
$result = $service->decorateMessage('TEST MESSAGE');
$this->assertSame(
'(MOCKED PREFIX) (ACTUAL SUFFIX)',
$result
);
}
Here we see the important bit: when one explicitly mocks one or a subset of methods, then the other methods are not mocked. Which is kinda obvious now I say it, but I guess we had been thinking one needed to call setMethods to be able to then mock the behaviour of the method. And, TBH, I don't think we thought through what was happening to the methods we were not mocking. I think we started calling setMethods all the time because we were slavishly following the PHPUnit docs, which always call it too, which is a less than ideal precedent to "recommend".
Generally speaking, one should want to mock all methods of a dependency, and it should really be the exception when explicitly not wanting to mock a method: ie, to leave it actually callable.
The win here is that we can now de-clutter our tests a bit. We really seldom need to call setMethods, and we sure want to make it clear when we do want to only mock some of a dependency's methods. So this will make our code cleaner and clearer, and easier to code review and maintain. Win!
Cheers Pete!
Righto.
--
Adam