I cannot write a series about avoiding mocks without mentioning Custom Assertion Matchers. If you don’t know what custom assertions are, here is pseudo code that uses a custom assertion :
That custom assertion matchers have an effect on mock usage might seem puzzling at first. Let me explain. Us, mere human developers, get lured into mocking when tests become too complicated. By keeping the tests simpler, Custom Assertion Matchers help use to avoid mocks. It’s a bit like why test data builders keep mocks at bay.
:bulb: We get lured into mocking when tests become too complicated
I already blogged about the benefits of Custom Assertion Matchers . Here I’m going to dive in their advantages against mocking.
This is the fifth post in a series about how to avoid mocks . If you haven’t yet, I recommend you to start fromthe beginning.
Why would we end up with mocks when we don’t have matchers ?
Let’s walkthrough a small story. Suppose we are building an e-commerce website. When someone passes an order, we want to notify the analytics service. Here is some very simple code for that.
class AnalyticsService def initialize @items =  end attr_reader :items def order_passed(customer, cart) cart.each do |item| @items.push(customer: customer, item: item) end end end class Order def initialize(customer, cart, analytics) @customer = customer @cart = cart @analytics = analytics end def pass # launch order processing and expedition @analytics.order_passed(@customer, @cart) end end describe 'Order' do it "notifies analytics service about passed orders" do cart = ["Pasta","Tomatoes"] analytics = AnalyticsService.new order = Order.new("Philippe", cart, analytics) order.pass expect(analytics.items).to include(customer: "Philippe", item: "Pasta") expect(analytics.items).to include(customer: "Philippe", item: "Tomatoes") end end
Let’s focus on the tests a bit. We first notice that the verification section is large and difficult to understand. Looking in more details, it knows too much about the internals of AnalyticsService. We had to make the items accessor public just for the sake of testing. The test even knows how the items are stored in a list of hashes. If we were to refactor this representation, we would have to change the tests as well.
We could argue that responsibility-wise, our test should only focus on Order. It makes sense for the test to use a mock to verify that the Order calls AnalyticsService as expected. Let’s see what this would look like.
it "notifies analytics service about passed orders" do cart = ["Pasta","Tomatoes"] analytics = AnalyticsService.new order = Order.new("Philippe", cart, analytics) expect(analytics).to receive(:order_passed).with("Philippe", cart) order.pass end
Sure, the test code is simpler. It’s also better according to good design principles. The only glitch is that we now have a mock in place with all the problems I describedbefore.
This might not (yet) be a problem in our example but, for example, the mock ‘cuts’ the execution of the program. Suppose that someday, the Order starts expecting something from the AnalyticsService. We’d then need to ‘simulate’ the real behavior in our mock. This would make the test very hard to maintain.
Matchers to the rescue
Let’s see how a matcher could help us here. The idea is to improve on the first ‘state checking’ solution to make it better than the mock one. We’ll extract and isolate all the state checking code in a custom matcher. By factorizing the code in a single matcher, we’ll reduce duplication. The matcher remains too intimate with the object, but as it is now unique and well named, it’s less of a problem. Plus, as always with matchers, we improved readability.
RSpec::Matchers.define :have_been_notified_of_order do |customer, cart| match do |analytics| cart.each do |item| return false unless analytics.items.include?(customer: customer, item: item) end true end end describe 'Order' do it "notifies analytics service about passed orders" do cart = ["Pasta","Tomatoes"] analytics = AnalyticsService.new order = Order.new("Philippe", cart, analytics) order.pass expect(analytics).to have_been_notified_of_order("Philippe", cart) end end
Here is how we could summarize the pros and cons of each approach :
|:-1: duplicated code||:-1: duplicates the program behavior||:heart: customizable error messages||
|:-1: breaks encapsulation||:heart: more readable||
|:-1: intimacy with the asserted object||
|:heart: factorizes the assertion code||
Depending on your situation, you might find further design improvements. In our example, a publish-subscribe pattern might do. A better design is likely to fix the encapsulation problem of the matcher. Here again, the custom assertion matchers will help. In most cases, it will be enough to change the implementation of the matchers only.
:bulb: Custom assertion matchers make refactoring easier by factorizing test assertions.
Summary of small-scale techniques
I’m done with small scale mock avoiding techniques. To summarize, the first thing to do is to push for more and more immutable value objects . Not only does it help us to avoid mocks, but it will also provides many benefits for production code. Practices likeTest Data Builders and Custom Assertion Matchers simplify dealing with Immutable Value Objects in tests. They also help to keep tests small and clean, which is also a great thing against mocks.
In the following posts, I’ll look into architecture scale techniques to avoid mocks. I’ll start with Hexagonal architecture.