Three Reasons To Roll Your Own Mocks

Three Reasons To Roll Your Own Mocks

Josh Cheek
Josh Cheek

November 28, 2011

I've spent the last several months writing my own mock objects, but for the code I'm currently working on, we were mostly using RSpec's built in dynamic mocking library. I paid attention to see how effective it was and if I would change my mind about it, but after a couple of weeks, I'm still pretty strongly in favour of rolling my own. Here are the three biggest reasons that I've noticed for preferring this approach.

1. It enables simple tests.

Tests can be an asset or a liability. I strongly prefer them in the asset category. One of the ways to turn them into liabilities is with long complex tests. I found that when doubling an object that needed several methods to be mocked out, this extraneous setup is necessary for the object to behave correctly, but irrelevant to the specific test they must be written in, and distracting when you later want to reread the test.

For example, lets say you want to specify the behaviour around submitting requests to a warehouse to send their inventory to your client. But it turns out your client is a military organization, and they protect all such requests with an encrypted key that changes every half hour and must be submitted with each request. What you want to test is something like "when the first warehouse doesn't have sufficient inventory, it requests the rest of the inventory from the second warehouse" but when you go to look at the tests, you'll see all this code dealing with setting up the key that it is going to need before it can send the request. That is essentially spam, it makes it harder to get the relevant information out of the test, making it a chore to read them.

If you roll your own, you can set it up with a sensible default such that this information is already taken care of.

2. It keeps the setup in one place, abstracted away from the tests.

When you make changes, many tests may break. Perhaps you change the name of a method that it invokes on its dependent object. Now you must go update all your dynamic mocks to change the name of the method. But if you have written your own mock object, then you only need to go change it in the one location where you've defined what methods it presents. Thus the tests do not need to be changed, they are still correct.

Or, using the previous example, what if the warehouses implement the key requirement at some point in the future. Now you need to go retrofit all your tests to add the key. But if you have rolled your own, then you can just give them a default value and not need to update each test. All your tests will pass without any changes (you'll, still need to add new tests for the stories around this new feature, but all the old ones will still valid without any change). Here is a simplified example of what that might look like.

 1module Mock
 2 class Warehouse
 3 def initialize(initial_quantity)
 4 @quantity_requested = 0
 5 end
 7 def request_inventory(quantity)
 8 @quantity_requested += quantity
 9 end
11 def has_been_asked_for?(quantity)
12 @quantity_requested == quantity
13 end
14 end
16 class Store
17 def initialize
18 @quantity_sent = 0
19 end
21 def send_inventory(quantity)
22 @quantity_sent += quantity
23 end
25 def has_been_sent?(quantity)
26 @quantity_sent == quantity
27 end
28 end
31describe InventoryOrderer do
32 let(:warehouse) { 15 }
33 let(:store) { }
34 let(:orderer) { }
35 before { orderer.add_warehouse warehouse }
37 it 'requests the inventory from the warehouse' do
38 orderer.place_order 10, store
39 warehouse.should have_been_asked_for 10
40 end
42 it 'sends the inventory to the store' do
43 orderer.place_order 10, store
44 store.should have_been_sent 10
45 end
46 # ... more tests for things like inventory type or not enough inventory ...

Now what happens when we add the key? We have to add the new tests around the key, and we have to update the mocks to reflect the new interface, but we do not have to update the tests. We have focused the test to the specific behaviour we are interested in, and changes to these interfaces won't require combing through spec files, updating many tests as we go.

 1module Mock
 2 class Warehouse
 3 def initialize(initial_quantity)
 4 @quantity_requested = 0
 5 end
 7 def request_inventory(key, quantity)
 8 @request_key = key
 9 @quantity_requested += quantity
10 end
12 def has_been_asked_for?(quantity)
13 @quantity_requested == quantity
14 end
16 def was_requested_with_key?(key)
17 @request_key == key
18 end
19 end

3. It consolidates the entire interface expected of the object being mocked

I greatly prefer having the interface expected of the object defined in one location. When you roll your own mock objects, you only need to go look at the mock to see what methods this object is presenting and you will know what is expected of any object wishing to fill that responsibility.

This consolidation helps us follow the Interface Segregation Principle Like in many statically typed languages, it presents the interface that any given object wishing to fill that role must implement. In dynamically typed languages like Ruby, there are no explicit interfaces, causing every object to implicitly be its own interface. This conceals the violation of ISP in cases like ActiveRecord::Base, leading to highly coupled, volatile classes whose interfaces are difficult to take alone or consider independently.

A consolidated interface is particularly useful if you develop the way I prefer to, where you first define the class using the object, and then later define the object itself. With this approach, when you need to figure out what methods to implement, you just go look at the mock object definition. When you use a mocking framework, you must go through each test to see what methods you're defining. This makes them easy to miss as they are distributed all across the source code.


Now I could foresee a mocking framework that fits well with the way that I like to roll my own mocks. Unlike the dynamic mocking frameworks, it would probably provide an API like FactoryGirl's which would allow it to present the advantages that I listed above. But until then, I'll probably continue to prefer rolling my own.