Mocking at architectural boundaries: persistence and time
More and more I've come to realize that I've been mocking less and less.
The thing is, creating test doubles is a very dangerous activity. For example, what I often see is something like this:
$entityManager = $this->createMock(EntityManager::class);
$entityManager->expects($this->once())
->method('persist')
->with($object);
$entityManager->expects($this->once())
->method('flush')
->with($object);
Or, what appears to be better, since we'd be mocking an interface instead of a concrete class:
$entityManager = $this->createMock(ObjectManagerInterface::class);
// ...
To be very honest, there isn't a big different between these two examples. If this code is in, for example, a unit test for a repository class, we're not testing many of the aspects of the code that should have been tested instead.
For example, by creating a test double for the EntityManager
, we're assuming that it will work well with any objects we'll pass to it. If you've ever debugged an issue with an EntityManager
, you know that this is a bad assumption. Anything may go wrong: a mistake in the mapping, missing configuration for cascading persist/delete behavior, an issue with the database credentials, availability of the database server, network connectivity, a missing or invalid database schema, etc.
In short, a unit test like this doesn't add any value, except that it verifies correct execution of the code you wrote (something a linter or static analysis tool may be able to do as well). There's nothing in this test that ensures a correct working once the code has been deployed and is actually being used.
The general rule to apply here is "Don't mock what you don't own" (see the excellent book "Growing Object-Oriented Software, Guided by Tests", or an article on the topic by Eric Smith, "That's Not Yours"). Whenever I've brought up this rule in discussions with developers, I've always met with resistance. "What else is there to mock?" "Isn't mocking meant to replace the slow, fragile stuff with something that is fast and stable?"
Mock across architecturally significant boundaries
Of course we want to use mocks for that. And we need to, since our test suite will become very slow and fragile if we don't do it. But we need to do it in the right place: at the boundaries of our application.
My reasoning for "when to mock" is always:
- If you encounter the need for some information or some action that isn't available to you in the memory of the the currently running program, define an interface that represents your query (in case you need to know something) or command (in case you want to do something). Put the interface in the "core" of your application (in the domain or application layer).
- Use this interface anywhere you want to send this query or command.
- Write at least one implementation for the interface, and make sure all the clients of the interface get this implementation injected as constructor arguments.
Mocking "persistence"
To fix the EntityManager
example above we need to take a step back and articulate our reason for using the EntityManager
in the first place. Apparently, we were in need of persisting an object. This is not something the running application could do naturally (the moment it stops, it forgets about any object it has in memory). So we had to reach across the application's boundaries, to an external service called a "database".
Because we always considering reusing things that are already available in our project, we just decided to go with the previously installed EntityManager
to fulfill our needs. If however we would've followed the steps described above, we would've ended up in a different place:
I need to persist (not just any object, but) my
Article
entity, so I define an interface that represents the action I intend for it to do:interface ArticleRepository { public function persist(Article $article): void }
I use this interface everywhere in my code.
I provide a default implementation for it, one that uses my beloved
EntityManager
:final class ORMArticleRepository implements A
Truncated by Planet PHP, read more at the original (another 6880 bytes)