Many people see mocks as a necessary evil to isolate their test code from third party dependencies and the outside world (the database, network, filesystem, etc). But in the paper “Mock Roles, Not Objects“, some of the first people to describe mocks describe them as a tool used in TDD to discover good interactions between your objects (i.e. design good types). They are much more powerful, and their costs are more reasonable, when they are used as a design tool, and not just a convenience tool for isolating your tests.
Note: “Mock” is a loaded word often used to describe any type of test double, but this article will be speaking about mocks in the strict sense. If you don’t know what that means, first read The Little Mocker by Uncle Bob. It’s the best explanation I’ve seen of the different types of test doubles. Further note: all of this will also apply to some implementations of spies.
To understand how mock objects can be used as a design tool, it helps to to think about object-oriented programming as being all about messaging. In OOP, we don’t just have procedures that we can call, we have objects that we can ask questions or give commands to. Those questions and commands are messages that we send to the object. When you write
my_model.save(), try thinking of it as telling the
my_model object to save itself.
So if OOP is about messages, what are mock objects used for? Verifying messages! You should use a mock when you are testing something that interacts with another object, and you want to verify that you have told that other object to do something – i.e. assert that you sent it a particular message. And when you are writing your test first, you literally get to make up what that message looks like.
This is how mocks are used as a design tool in TDD. You work outside-in: start at a high level, and delegate details to lower levels. Mock those lower levels because right now you only care about telling them what to do. You’ll worry about how they do it later, when you’re ready to test that level.
In other words, you design your messages from the perspective of the message sender, the perspective that cares most about what you want that object to do, and least about how that object does it. This leads to messages that are simple and communicate well. And that leads to an object API that is simple and communicates well.
When done right, it feels like cheating. Your high-level tests almost feel like they aren’t testing anything. That’s good. These high level tests aren’t about verifying algorithms or reducing bugs. They are about designing your messages. It’s part of a TDD process to design code that is easy to understand and maintain. This high level code is easy to test because it’s easy to understand. It also helps lead to low-level code that is easy to test and understand because you’ve shaken out all the object collaboration in the higher levels, leaving simple procedures that can be tested without mocks.
But you only get these design benefits if you own the API of the object you’re mocking. You may have heard that you should not mock what you don’t own. Some libraries even strictly enforce this rule. But what does that mean? Why is it important?
When you “mock something you don’t own”, like a third-party dependency or something in stdlib, you can’t let your tests help you decide what the messages should be, because those choices have already been made. So if you only use mocks in this way, you are only getting what should be a side-effect of mocking, with none of the design benefits. And that leads to pain, because mocks have high costs. They give you plenty of rope to hang yourself with: increased coupling between test and implementation, potential for “false positives”, and increased setup costs. Many people don’t like mocks for these reasons, and if you aren’t using them primarily to design messages, I agree, they aren’t worth it.
So how do you mitigate those costs? What exactly should you do when you have an external dependency? What does this all look like in practice? I’m still writing about those topics and more, and planning to release it as a series about mocking and TDD. Part 2 covers mocks and external dependencies. If you’d like to be emailed when it is complete, subscribe to my newsletter. In the meantime, try using mocks to design the interactions between your objects. Used in this way, they can become a powerful part of your TDD tool belt.