Mock objects can be very useful in test-driven design, but they need to be used wisely.
A mock object is a testing tool that acts as a stand-in for a “real” object during testing in much the same way as an Elvis impersonator stands in for the real King. The impersonator is cheaper, easier to access, and most likely lighter weight.
Mock objects have a lot of uses in a test-driven process, including replacing unreachable external resources and speeding up tests. One of the most important uses of mock objects in a test or behavior-driven process is to help design and specify the application as you write it. In particular, mock objects can be used to define the API between different layers of your application. In this article I’ll use core Rails testing features plus the Mocha mock object gem to demonstrate how mock objects can be used as design tools.
A Brief Field Guide to Mock Objects
There are two primary types of mock objects, most commonly called stubs and mocks. Think of them as passive versus aggressive ways of specifying fake values. Stubs are passive: when you declare a stub, you specify an object, a method, and a value. In Mocha, a stub declaration might look like this:
From that stub declaration forward, when the thing#name method is called, the value "Fred" is returned, and the actual declaration of the method is not touched. That’s a helpful feature if the actual method calls PayPal, or some other remote or dangerous object. (Mocha has a lot of options to augment the method and return value declarations, but for our purposes here we can ignore that complexity.)
When I describe a stub as “passive,” I mean the stub doesn’t care whether you actually use it. If I make the above declaration, but never call thing.name, nothing particularly bad happens. In contrast, a mock is more aggressive. (Yes, “mock” is used both as the name of the class of objects as a whole, and the name of one particular type. Yes, it’s confusing.) You can declare a mock in mocha with a very similar syntax:
When you use the expects method, the behavior of the object in the test changes. When the mock expectation is declared in a test, if the thing.name method is not called in that test, the test will fail. Unlike a stub, a mock actually does care whether it is called as part of the test—you are not just specifying a value for the method; you are making a testable assertion about the behavior of your application.
While the difference between stubs and mocks may seem subtle, it leads to significant change in the structure of your test. Since the values returned by the mock are placed there by you, they are useless in determining whether your application is achieving a desired state. Instead, the mere fact of the call to the mock becomes a test about the behavior of your application, and the correctness of the method being called is deferred to a test that actually focuses on that method. Do it correctly and you get faster tests that have a clear separation of responsibilities. You also get a better application design.
A Messy Test
One of the best places to use the mock style of testing is to keep tests from jumping between layers of the Rails stack; specifically, keeping controller tests from being dependent on the model layer. Using a non-mock style, your controller methods under test inevitably call the model layer. Here’s an example from a hypothetical program that is showing status for all members of a project.
If you’ve done controller testing from Rails, you are probably familiar with the basic style. The test does some setup, calls a controller action, and then makes assertions about the state of the system after the controller call.
What’s interesting for our purposes is the dependency between this controller test and the model layer. This test says nothing about the API of the model object being called, but is presumably dependent on the behavior of that model object to create the state being verified in the last lines of the test.
In a mock testing setup, that dependency between the controller and the model is flipped exactly backward. You actively want the controller test to specify that the API between the controller and model is simple and clean. Conversely—and this is the tricky part—the controller test shouldn’t actually care about the behavior of the model in order to pass.
As written, this test cannot pass without the model method written and working. In a TDD environment, that means you’re going to need to write model tests that would largely duplicate the behavior of this controller test. It’s a duplication of effort, and it increases the number of tests that will fail if the model method is broken.
Mocks to the Rescue
Let’s try the test again, this time using Mocha to define mock objects:
At first glance, it seems as though there’s almost nothing going on in this test: it barely seems to be asserting anything other than the idea that the reports variable is assigned to... something. The trick to this test is the mock expectation on the reports_grouped_by_day method, This test validates that the controller method runs without error and calls the model method reports_grouped_by_day exactly once. The test is validating a behavior of the controller method, not the state that results from making that call.
What this test doesn’t do is attempt to validate features that are actually the purview of other tests. It doesn’t validate the response from the model method; that’s the job of the model test itself. What the view layer does with this value is the job of a view test. This test validates that a particular instance variable is set to a value using a known model method, on the theory that the job of the controller method is to produce a set of known values for use by the view. But validating the exact value of the :reports variable would be pointless (at least in this case), since the value is completely generated by the mock expectation. Validating the reports method would be testing the state of the application, but in the mock style, the state is provided by the test itself.
As promised, the mock test flips the dependencies of the standard test. The mock test doesn’t care about the return value of the model method—it doesn’t even care if the model method exists. At the same time, this test makes a statement about the model classes API and, by extension, the design and structure of your application.
Caution: Danger Ahead
One of the features of the mock controller test is that it’s isolated from the behavior of the model method. This is good, in that it makes the test more focused. But if the test passes even if the model method doesn’t exist, well, what’s to prevent us from shipping the app without the model method? Not this test, that’s for sure.
Acceptance testing, automated or otherwise, is one way to avoid this problem. You need to have some way to do an end-to-end verification of your system. Another is just to trust the process and let the existence of this mock expectation drive the creation of the model functionality in the same way that a failed test might.
When using a mock object style, it’s critically important to remember that you are designing your classes through your tests. If it seems like the test needs too many mock expectations to run, that’s a strong sign that your code needs to be refactored; often, multiple mocks are a sign there’s an abstraction that needs its own class, or a method all to itself. The more mocks in your tests, the more brittle they are against changes or refactoring in your code. Luckily, minimizing the mocks you write effectively minimizes the points of contact between parts of your application, leading to low-coupling designs that are easier to maintain and extend over time. However, the mock style of testing can be difficult to apply to a legacy system, in which poorly structured relationships between objects may already be built into the code.
Try It Yourself
If you’ve never tried a strictly mockist style of testing, you’ll probably find it a challenge at first. Just remember the guidelines for a strict mock style: everything outside the method under test should be represented by a mock; no single code error should break more than one test; and use the shape of the mocks you create to inform the structure of your application.
Noel Rappin is a Senior Consultant at Obtiva. A Rails developer for five years, Noel has spoken at RailsConf and Windy City Rails, and is the author of the forthcoming Rails Test Prescriptions. A blog relating to this book can be found at www.railsrx.com.