Mocking isn't evil, but avoid it anyway
Mocks vs fakes
Mocks and fakes (not to mention stubs, spies, and more) are test doubles. They are “pretend” objects that stand in for real objects to facilitate testing your code.
There are lots of blog posts out in the wild that cover mocks vs fakes. Most of them are critical of mocks and encourage fakes instead. My take, which is mostly in alignment with the general zeitgeist, is this:
Mocking isn’t inherently evil, but it’s almost always associated with evil.
Anti-pattern: testing your implementation details
For reasons I don’t fully understand, 10-20 years ago the industry became obsessed with class-level unit tests. Think: a test creates an instance of Foo
, passing in mock implementations of every one of its dependencies Bar
, Baz
, etc; the test calls Foo
’s public methods, then asserts that particular methods were called on Bar
, Baz
, etc.
I think this is an anti-pattern. It’s asserting more about the implementation details of your code architecture than it is about the functional effects of the code. If you’ve ever decided to refactor a few classes and spent more time fixing test mocks than on the refactor itself then you’ve experienced one of the downsides of this style of testing.
Mocking enables this anti-pattern. It makes it really easy to commit the sin of testing your implementation details.
Best practice: test your public API
In most cases, what you probably want to do is focus on tests that are higher level, exercising the public API of your service or library as a whole, and asserting on the expected effects (like a particular response was returned, a particular thing got written to a DB, etc). Mostly real code is used, with fakes replacing the outer edges (typically the things doing I/O, like making a request to another service or writing to a DB). Depending on how good the fake is, your tests might not need to care that a fake is being used at all.
Examples: An in-memory DB is a really good fake DB. A well-written fake FooBar client might behave just like the real FooBar service (maybe it even uses much of the same code internally!).
This style of testing produces tests that:
- Don’t break due to simple refactors of implementation details
- Verify that your service or library actually works when the classes are integrated
- Act as a sort of documentation, showing the expected inputs and outputs of your service or library
The blurry line between mocks and fakes
Sometimes you can’t or don’t want to create a really good fake, or there’s no behavior worth faking. For example, if you just want to check that an event was emitted into Kafka as a side effect of some API call, then all you really need to do is check if your fake kafka client received a message.
Which brings us to “mocking isn’t inherently evil”, which is where my opinion diverges slightly from the collective. For cases like this, where you just want an object to receive a method call in a way that you can verify, then in my opinion mocking is fine. It’s just a way to produce a test double using (arguably) less boilerplate than writing a thin fake by hand.
The key lesson here is that mocking itself isn’t the anti-pattern; testing your implementation details is the anti-pattern. However, culturally, the practice of mocking has become linked with testing your implementation. Consequently, I rarely if ever use mocks anymore, preferring instead to just create thin fakes where I need them. I encourage you to do the same, or to at least be very vigilant in your use of mocks, lest the anti-pattern follow in its wake.