In this post I share 5 easy ways to write better mocks that I picked up over the years. These will help you write tests that break less, are easier to read, are more IDE friendly, and are easier to refactor. The focus is on PHPUnit and PHP, yet most of the techniques used, and principles touched upon, are also applicable when using different languages and testing frameworks.
Before we get down to it, some terminology needs to be agreed upon. This post is about Test Doubles, which are commonly referred to as Mocks. It’s somewhat unfortunate that Mock has become the common name, as it also is a specific type of Test Double. I will use the following, more precise, terminology for the rest of the post:
- Test Double: General term for test code that stands in for production code
- Stub: A Test Double that does nothing except for returning hardcoded values
- Fake: A Test Double that has real behavior in it, though does not make any assertions
- Spy: A Test Double that records calls to its methods, though does not make assertions
- Mock: A Test Double that makes assertions
1. Reference classes using ::class
This one is really simple. Instead of calling
$this->getMock( 'KittenRepository' ) , use the ::class
keyword added in PHP 5.5:
$this->getMock( KittenRepository::class ) . This avoids your tests getting broken when renaming or moving your class using decent editors. Typos are immediately apparent, and navigating to the class or interface becomes easier.
2. Don’t bind to method names when you don’t have to
Imagine you are testing some code that uses a PSR-3 compliant logger. This logger has a general log
method that takes a log level, and a specific method for each of the log levels, such as warning
and info
. Even in case where you want to test the specific log level being used, the code under test can use either log or the more specific method. Which one is used is an internal implementation detail of the production code, and something the test preferably does not know about. Consider this Mock:
1 |
$logger->expects( $this->never() )->method( 'log' ); |
If your production code changes to use a more specific method, the test will no longer be correct. In this case you might not even notice, as the test does not further rely on behavior of the created Test Double. Another cost to consider is that you have a string reference to a method name, which amongst other things, breaks refactoring.
In a number of cases this is easily avoided using the not so well known anything
PHPUnit method. Want to verify your logger is never invoked in a given situation?
1 |
$logger->expects( $this->never() )->method( $this->anything() ); |
Want to test what happens when the repository your code uses throws an exception?
1 2 |
$repository->expects( $this->any() )->method( $this->anything() ) ->willThrowException( new RuntimeException() ); |
This approach only works in some situations. In others you will either need to bear the cost of binding to implementation details, change your test to be state based, or resort to more complicated workarounds.
3. Don’t bind to call count when you don’t have to
When constructing a Stub or a Fake, it’s easy to turn it into a Mock as well. This is very similar to binding to method calls: your test becomes aware of implementation details.
The previous code snippet shows you a very simple Stub. It’s a logger that always throws an exception. Note the use of the any
method, as opposed to the once
method in this snippet:
1 2 |
$repository->expects( $this->once() )->method( $this->anything() ) ->willThrowException( new RuntimeException() ); |
If you are not intentionally creating a Mock, then don’t make assertions about the call count. It’s easy to add the assertion in nearly all cases, yet it does not come for free.
4. Encapsulate your Test Double creation
When constructing a Test Double via the PHPUnit Test Double API, you get back an instance of PHPUnit_Framework_MockObject_MockObject
. While you know that it is also an instance of the class or interface that you fed into the Mock API, tools need a little help (before they are able to help you in return). One way of doing this is extracting the Test Double creation into its own method, and using a return type hint. If you are still using PHP 5.x, you can add a DocBlock with @return KittenRepository
to achieve the same effect.
1 2 3 4 5 6 7 8 |
private function newThrowingRepository(): KittenRepository { $repository = $this->getMock( KittenRepository::class ); $repository->expects( $this->once() )->method( $this->anything() ) ->willThrowException( new RuntimeException() ); return $repository; } |
Now tools will stop complaining that you are giving a MockObject
to code expecting a KittenRepository
.
This extraction has two additional benefits. Firstly, you hide the details of Test Double construction from your actual test method, which now no longer knows about the mocking framework. Secondly, your test method becomes more readable, as it is no longer polluted by details on the wrong level of abstraction. That brings us to clean functions and test methods in general, which is out of scope for this particular blog post.
5. Create your own Test Doubles
While often it’s great to use the PHPUnit Test Double API, there are plenty of cases where creating your own Test Doubles yields significant advantages.
To create your own Test Doubles, simply implement the interface as you would do in production code. For stubs, there is not much to do, just return the stub value. Some tools even allow you to automatically create these. Spies are also quite simple, just create a field of type array to store the calls to a method, and provide a getter.
Remember how we do not want to bind to our production code’s choice of logger method? If we want to assert something different than no calls being made, the PHPUnit Test Double API is not of much help. It is however easy to create a simple Spy.
1 2 3 4 5 6 7 8 9 10 11 |
class LoggerSpy extends \Psr\Log\AbstractLogger { private $logCalls = []; public function log( $level, $message, array $context = [] ) { $this->logCalls[] = [ $level, $message, $context ]; } public function getLogCalls(): array { return $this->logCalls; } } |
Since AbstractLogger
provides the specific logging methods such as warning
and info
and has them call log
, all calls end up looking the same to the test using the Spy. This class is now available as a mini-library.
The test that uses the Spy needs to make its own assertions on the spied upon method calls. If certain assertions are common, you can place them in your Spy. Since the Spy itself does not invoke these assertions, it remains a Spy and does not become a Mock. You can even use PHPUnit to do the actual assertions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class MailerSpy implements Mailer { private $testCase; private $sendMailCalls = []; public function __construct( PHPUnit_Framework_TestCase $testCase ) { $this->testCase = $testCase; } public function sendMail( EmailAddress $recipient, array $templateArguments = [] ) { $this->sendMailCalls[] = func_get_args(); } public function assertMailerCalledOnceWith( EmailAddress $expectedEmail, array $expectedArguments ) { $this->testCase->assertCount( 1, $this->sendMailCalls, 'Mailer should be called exactly once' ); $this->testCase->assertEquals( [ $expectedEmail, $expectedArguments ], $this->sendMailCalls[0] ); } } |
Creating your own Test Doubles completely sidesteps the problem of referencing method names using strings. I’ve yet to see a tool that understands the Test Doubles created by PHPUnit. Your IDE won’t find them when you search for all implementors of an interface, making refactoring, discovery and navigation harder.
A related advantage is that lack of magic not only makes the code easier to understand to tools, but also to developers. You do not need knowledge of the PHPUnit Test Double API to understand and modify your own Test Doubles.
In my projects I put Test Doubles into tests/Fixtures
. Since I have a dedicated class for each Test Double, it’s easy to reuse them. And the tests in which I use them focus on what they want to test, without being polluted with Test Double creation code.
Frank de Jonge wrote a longer take on why to not use mocking tools.
Wrapping up
Treat your tests as first class code. Avoid not needed dependencies, respect encapsulation as much as you can, try not to use magic, and keep things simple. The ::class
keyword, the any
and anything
methods, encapsulated Test Double construction, and creating your own Test Doubles, are all things that can help with this.
5 thoughts on “5 ways to write better mocks”