Mocking libraries are an antipattern

Mocking libraries are an antipattern

This post sets out the case that mocking libraries are usually not needed, and more often a hinderance to development and an indication that the production code is poorly factored. These libraries are common in the world of enterprise programming.

First, some definitions:

  • A stub is a dummy object used in automated tests, providing configurable return values.
  • A mock is akin to a stub, but performs assertions on the calls made to the object.
  • A test double refers to any kind of stub, mock, etc – think of it as a stunt double.
  • A mocking library is a library which provides a way to create these objects on the fly. A random example: Mockito
  • An antipattern is a repeated behaviour that looks helpful at first, but actually causes more problems than it solves.

A world without mocking libraries

Say your UserService interacts with your UserRepository, and you want to stub the latter in order to test the former. Without a mocking library you might implement an InMemoryUserRepository, with CRUD methods.

Is that such a bad place to be? It may feel like you're having to write more code, and perhaps you need to write some simple unit tests for the in-memory repository, but you can then express your setup code more naturally:

userRepo.when(r => r.getUser(user.id)).return(user)

vs

userRepo.insert(user)

But inline declaration seems useful?

The above is a simple example. Sometimes a dummy object can't be implemented so easily. This is where mocking libraries seem most useful: you can declare inline how you want a method to respond.

userClient.when(c => c.get("/users/" + id + "/avatar")).return(avatar)

The mocking library's power lies in the fact that this can be written in the test body, colocating it with relevant code.

Tight coupling damages velocity

If your test suite is comprehensive, as it should be, you will likely end up repeating this stubbing code many times over. There is a problem here: the tests become tightly coupled to their subject's dependencies. This may seem acceptable since this is "only test code", but unfortunately the same problems arise in test code as in production code when trying to make changes.

What happens to the above example when you need to change userClient.get? Perhaps another parameter is needed, or the shape of the response changes. You'll need to update the production call site(s) – and all of the test stubs. If you've wrapped your whole codebase in unit tests and stubs, changing interfaces becomes an uphill struggle and your velocity decreases. Refactoring becomes painful and your codebase stagnates.

What can be done about this? Perhaps a helper method in the test suite:

def stubAvatar(id: UserId, avatar: Avatar) = {
  userClient.when(c => c.get("/users/" + id + "/avatar")).return(avatar)
}

This looks like a good way to decouple the test from its dependencies: when the get method changes, we only have to update one place in the test code.

Full circle

Wait a minute - if we're writing dedicated code for each test stub, haven't we just arrived at another in-memory implementation of the dependency? For the UserClient example, we can just write a reusable StubbedUserClient with helper methods to stub out responses. Remind me why we needed the library at all?

Test doubles as a barometer of code reuse

If creating dedicated test doubles seems like too much work because each of your interface's methods is used only a handful of times, think about what that says about your application structure. The more you follow SOLID principles, the closer you will get to a homogenous codebase with interfaces that are reused all over the place. A test double implementation of a widely-reused interface is much more useful than one used in just one or two consumers.

This is why I believe that mocking libraries are an antipattern - or at least a code smell. In a codebase with well-factored code, following SOLID principles, it seems unlikely to me that a mocking library would be required.


Image credits via Unsplash: