Opinions about mock objects are not hard to find. Rarer is the kind of solid practical advice on using mocks that Zach offers here.
Mocks have long been a source of question and confusion for a lot of people, myself included (see "Mocks? Really?"). Years ago I embarked on a journey to make sense of mocks and add them to my toolbox. I had almost entirely forgotten about this voyage when I was in Charlotte a few weeks ago eating lunch with Jim Van Fleet and Matt Overstreet and Jim asked, “When do you use mocks?” It started me thinking about the legendary tales of mocks.
Mock objects, as you no doubt know, are simulated objects designed to mimic the behavior of real objects for testing purposes. Mocks are also a subject of software development controversy and folklore. From the folklore surrounding mocks, you might think that clarity and mocks are mutually exclusive. We’ve likely all heard a story or two of how interaction-based testing with mocks changed lives. Sometimes it’s a story of change for the better and sometimes for the worse. So which is it, folklore? Better or worse? Are mocks useful tools to employ in our everyday pursuit of building useful software? Or are they a failed experiment that we should walk away from and not look back? These are the questions I asked in my journey, and heard others ask.
As with most things, the usefulness of mocks isn’t a matter of black or white. Mocks are just another tool, and using them under the right circumstances is what makes them valuable. Just as using a cross-cut saw to hammer nails won’t provide much value in building a house, using mocks in every unit test won’t provide much value in building software.
When to Mock
So it’s not a question of whether to use mocks, but of when it might be useful to use them. Over the years I’ve seen cases where mocks were appropriate and where they were not.
Consider this example from when I was implementing a caching mechanism for a Ruby on Rails library. One behavior in the library worked like this: after a method was called on an object which delegated to another object the result would be cached and used for subsequent calls. To ensure this functionality worked as expected I used a mock. It was the simplest technique for testing the caching behavior. More on this later.
In contrast, consider this example. We were working on an email parser which pulled out the original sender and receiver from a forwarded email. We wanted this to be represented in the same way we handled the sender and receiver of a non-forwarded email. It’s common for emails to not only include the email address, but also a display name and potentially a mailto link. In the implementation we ended up with two separate objects: an EmailParser and an EmailAddress. However, in our test we didn’t use mocks to verify that the EmailParser constructed EmailAddresses. Instead we wrote the examples in our test to expect a particular API. We just happened to use an EmailAddress object to satisfy the API we expected. Using a mock here would have worked, but it would have brought along some additional trade-offs we didn’t feel were necessary for driving this behavior.
I’ve described these cases in some detail because it all comes down to the details. If a few of the details in these situations had been different it’s likely our decision to use or not use mocks would have been different. Okay, but why, exactly? Is there some general principle that can help us decide when to use mocks?
The most relevant guideline I’ve found is in something that Dan North said about Behavior Driven Development (BDD) in The RSpec Book: that a characteristic of doing BDD is recognizing that software is “all about behavior.” This same idea has tremendous power when used as a guideline for using mocks. If caring about an implementation detail doesn’t help you drive or ensure the behavior of your code, then you shouldn’t use a mock. This simple qualifier will save you and future maintainers of your application’s test suite a lot of time and frustration.
I want to expand on that with some specific examples, but before we dive in, let’s make sure we are using our words in the same way.
What’s a Mock?
The term “mock” refers to an object that is used to set expectations on, which are later verified. If the expectations are not met, then a mock raises an exception indicating failure.
A mock expectation refers to a method that is expected to be called on a mock. These expectations often include expected method arguments. Other properties of expectations can include indicating the number of times the method should be called, which may include never, as well as a return value.
How Do You Use a Mock?
You’ll find more detailed documentation for how to use a mock in your mocking library’s documentation, but should you not have that handy, here’s a quick overview that will put us on the same page.
A basic RSpec example using a mock:
According to this example, when Cesar Millan commands the dog, the dog obeys. If the dog doesn’t obey, the example will fail. If the dog does obey, the example will pass. You’ll notice that there is no code to verify the expectation. This is because most modern mocking libraries do this for you after each test run.
To see how this example can be satisfied, here’s a passing implementation:
Our code does just what the example specified. When Cesar commands the dog, the dog obeys.
Mock Expectations Outside of a Mock Object
We just used a dedicated mock object. Now let’s expand our understanding of how mock expectations can be used. In more dynamic languages like Ruby, an expectation can be set on any object, not just mock objects.
An example is worth a thousand paragraphs so here’s an updated RSpec example to use a real Dog object instead of a mock(“dog”):
Here we used an actual Dog object instead of creating a mock(“dog”). This technique is often referred to a partial mocking, meaning that we’re using mock expectations outside of a dedicated mock object. This allows us to use real objects from our own applications and only mock out expectations that we care about.
Okay, hopefully this primer has been enough to build a shared understanding for the concepts of a mock. Now let’s look at Jim’s question. Remember Jim’s question?
When Do You Use Mocks?
Generally, there is one overarching reason for why you would use a mock. That reason is this: You care about an implementation detail in your code. And when do you care? You care when it drives behavior.
Let’s look at three specific examples where mocks have routinely been helpful.
Third-party/external systems integration
The use of mocks is by no means limited to this list, but these three examples represent types of situations where I’ve found mocks valuable.
Third-Party/External Systems Integration
Mocks are a great tool to use when writing tests around third-party/external systems integration in your application. It frees your unit tests from depending on the external system while allowing you to verify the application’s behavior for integrating with that system.
The primary trade-off with external systems is that for a more reliable unit test suite around how your application behaves, you get less coverage on integrating with the external application. This applies almost universally to unit tests only since unit tests are where you want to isolate sets of behavior in order to ensure that they are working as expected. However, unit tests without integration tests don’t cut the mustard. You would be well served to have at least an integration test that integrates with the external system without the aide of mocks.
Consider the DogWhisperer example. Imagine the dog.obey call connected to a web-service. Each time dog.obey gets called, that would need to connect to dog.com, authenticate as the proper dog, and then issue the obey command. This takes time to connect, handshake, send our request, and then receive a response. What if dog.com was down or simply timed out? We’d end up with really slow tests, a lot of potentially incorrect failures, and a lot of time spent investigating these failures.
Mocks serve us well here. They disconnect our code from being affected by things that are out of our control. Removing unnecessary variables allows our code to be repeatably and consistently exercised. We can be more confident that our code is working—or that when it breaks, it is actually broken. And we can have an integration test that verifies actual integration. We may still get a false positive, but we’ve minimized the number of failures to an extremely small number. Those failure(s) can quickly be investigated to determine if the integration broke or if all is well.
A secondary benefit of using mocks is that you get faster tests. This is a nice side effect of connecting to fewer external systems and resources and can be a great thing to keep in mind with your testing strategy. Also, this speed benefit isn’t limited to testing third-party/external systems. It exists in any situation where mocks can be used. Be cautious though: speed is not the most important thing—far from it, in fact. Mocks make your tests go extremely fast, but that kind of fast can be like putting a Ferrari on blocks and punching the gas.
If it comes down to having slower, meaningful tests or just fast tests, always choose slow and meaningful. If a meaningful test is slower than you’d like, but you’re out of ideas for the moment, write it down and come back to it later. I always find it helpful to talk it through with other developers.
In general, mocks provide a good value when dealing with third-party/external systems integration.
Caching, an Example of Caring about Implementation Details
Object oriented programming is all about encapsulation and information hiding. Conventional wisdom suggests that you shouldn’t care about the details. I wholeheartedly agree with this as a general rule of thumb. But experience has shown that sometimes you do care.
Consider the caching library example from the opening paragraphs of this article. I knew how I wanted the cache to work, but I needed a way to verify that it was working properly. Without mocks, this would have been difficult.
Here’s the RSpec example for the case we’ve been talking about:
This example is short, concise, and clear. Heck, it’s readable! There was no need to involve file systems, databases, or any other external resource. A mock was all we needed.
More often than not you won’t care about the implementation details; you’ll care about the behavior. In situations where caring about an implementation detail helps drive and verify the behavior, mocks can be a great tool. This caching illustration is an example of that.
Coordinator objects are at the heart of design patterns like Model-View-Controller and Model-View-Presenter. You may recognize them as the controllers in Rails applications and presenters in applications that follow the Presenter-First design pattern. They can commonly be spotted by their names as they routinely end in -Controller, -Manager, or -Presenter. They are coordinator objects because they are rarely responsible for doing any heavy lifting and are instead tasked with coordinating the interactions between two components.
To illustrate a coordinator object in practice, consider a Rails controller. Its job is to take an incoming web request and hand it off to the appropriate object to do the work. Once the work has been done, it’s responsible for initiating the communication back to the requesting web client. At no point should the controller become the place where all of the business logic and work takes place. Back in 2006 Jamis Buck coined the term Skinny Controller Fat Model which reinforces this concept that a controller is a coordinator and not a doer.
In the Presenter-First world, the presenter plays a similar role to the Rails controller. It listens to the events from the view and makes the appropriate calls on the models. Likewise, when models trigger events, the presenter listens and calls the appropriate methods on the view. The presenter exists to manage proper interactions between the views and models. No more, no less.
In traditional TDD you test-drive these coordinator objects into existence, often times using a mock to ensure the interactions being coordinated are what you want. A benefit of this is that it guides you to drive the APIs you want on the objects being coordinated and it helps keep unnecessary logic out of the coordinator. On the flip side, paying so much detailed attention to a coordinator takes a lot of time and care. I’m not convinced it’s always worth it.
Consider when the design benefit is most effective: when you aren’t experienced with making the kinds of design decisions which lead to skinny, narrowly focused coordinator objects. If that’s you, then the traditional TDD route with mocks can be a very good thing to practice. And by practice, I literally mean a 15–30 minute practice session. Through practice you experience pain points, have an opportunity to challenge your design thinking, rethink the design, and try new things. It is a catalyst to making good designs and avoiding painful ones. Out of it you’ll even begin to naturally apply principles like single responsibility and separation of concerns.
Deliberate practice with mocks and coordinator objects will help you to grow as a developer: of that there is no doubt. But on a daily basis while you’re performing the act of building software there are very real trade-offs with strictly following the traditional TDD route.
For instance, testing every Rails controller in isolation with mocks takes time and care to create and maintain. As the application evolves, you commonly find yourself making changes to meaningful tests and then also having to go back and make a change to a controller test. But why? To what benefit?
Coordinator objects like controllers are driven into existence because you need to hook two areas of your application together. In Rails applications, you usually drive the controller into existence using a Cucumber scenario or some other integration test. Most of the time controllers are straightforward and not very interesting: essentially a bunch of boilerplate code to wire your app together. In these cases the benefit of having isolated controller tests is very little, but the cost of creating and maintaining them can be high.
A general rule of thumb is this: If there are interesting characteristics or behaviors associated with a coordinator object and it is not well covered by another test, by all means add an isolated test around it and know that mocks can be very effective.
As Albert Einstein once said, “Make everything as simple as possible, but not simpler.” This applies not only to production code, but also test code. Therefore I want simple, concise, meaningful, and valuable test code. If another test provides adequate behavioral coverage that communicates well, I see no reason to make more work for myself or future maintainers of the code.
Mocks are just tools. No different than stubs, factories, or fixtures. What makes them valuable is using them in situations where they are effective. In order to be effective, they need to help drive and/or verify behavior of your application. When they are doing that, you will receive the most value out of the mocks, short-term and long-term. If they’re not doing that, get rid of them.
As we saw with third-party/external systems, integration mocks can be very effective in ensuring that our code responsible for integrating is behaving as expected. The value received by knowing details about the integration code outweighs the risk of not knowing.
In the caching example, we saw a case where caring about an implementation detail helped drive and verify behavior. Most of the time you won’t care about implementation details, but there are times when it makes sense. In those times mocks can be very helpful.
Lastly, we looked at how coordinator objects could benefit from and be impaired by the use of mocks. The ability to discern when to use isolated tests with mocks around coordinator objects can mean good, effective test coverage rather than something unnecessarily costly.
You may have noticed that the central theme throughout this article really isn’t mocks at all. It’s behavior. To quote Dan North again, a characteristic of doing BDD is recognizing that software is “all about behavior.”
So take this as a parting guideline for using mocks: a mock should help drive or ensure behavior of your code. This simple rule can go a long way.
Zach Dennis is a co-author of The RSpec Book and a co-founder and fellow human at Mutually Human Software, an expert custom software strategy and design consultancy in Grand Rapids, Michigan. He has been enjoying Ruby for nearly eight years and has contributed to several projects such as Ruby’s standard library documentation, Ruby on Rails, and RSpec. In his spare time, Zach loves spending time with his family, continuously learning, playing music, and running continuousthinking.com.