Mixing it up with Mocktail
Caleb Hearth
Posted on August 31, 2022
Mocktail is a new testing library from the lovely folks at Test Double. It seeks to provide a more modern, less intrusive, and friendlier interface for using test doubles (not to be confused with the authors) than prior art such as rspec-mocks, Mocha, rr, and other options currently available.
In the words of its author, the “huge runaway headline is that you can inject a fake into production code in one line and without bending over backwards to use dependency injection or mess with mocking #new
manually”.
In this article, we’re going to work through a TDD bartender class and because it’s on theme and a Friday, we’re going to have it mix us up a nice Vieux Carré. We’ll start really simply with an empty test case.
require 'minitest/autorun'
require 'mocktail'
class VieuxCarreTest < Minitest::Test
include Mocktail::DSL
def test_mixes_up_a_lovely_drink
end
end
Your app probably has a more complex setup, but note that in our case, our Gemfile is really simple:
source "https://rubygems.org"
gem "minitest"
gem "mocktail"
Our bartender is going to use a mixing glass, and add the ingredients for our drink:
def test_mixes_up_a_lovely_drink
glass = Mocktail.of_next(MixingGlass)
stubs { glass.stir }.with { :yummy_drink }
assert_equal Bartender.new.vieux_carre, :yummy_drink
verify { glass.add(:rye, amt: 0.75, measure: :oz) }
verify { glass.add(:cognac, amt: 0.75, mesaure: :oz) }
verify { glass.add(:sweet_vermouth, amt: 0.75, measure: :oz) }
verify { glass.add(:benedictine, amt: 0.5, measure: :tsp) }
verify { glass.add(:angostura, amt: 1, measure: :dash) }
verify { glass.add(:peychauds, amt: 1, measure: :dash) }
end
class MixingGlass
def stir; end
end
class Bartender
def vieux_carre
glass = MixingGlass.new
glass.stir
end
end
That first line is the magic Justin told us about earlier: the bartender is going to grab the mixing glass herself–we don’t need to hand it to her when we sit down and order. Because we’re not planning to use dependency injection to pass in the mixing glass, the line Mocktail.of_next(MixingGlass)
tells Mocktail to return a test double for the next Mocktail.new
call and gives us a reference to what it’ll return so we can make expectations on that.
The other really helpful thing that’ll happen when we run this test is that Mocktail will give us a great error message about the missing #add
method:
1) Error:
VieuxCarreTest#test_mixes_up_a_lovely_drink:
NoMethodError: No method `MixingGlass#add' exists for call:
add(:rye, amt: 0.75, measure: :oz)
Need to define the method? Here's a sample definition:
def add(rye, amt:, measure:)
end
We could copy that right into the class if we wanted to, and if we’d used :ingredient
instead of :rye
the signature would be perfect–good to keep in mind for next time.
Now that we’ve got the method signature in place, we can run the test again to see that of course we’re not actually calling add
anywhere. Let’s go ahead and write out what we’d like our API to look like and code up the recipe steps in Bartender. We’ll intentionally make some mistakes in the recipe to see what Mocktail tells us:
class Bartender
def vieux_carre
glass = MixingGlass.new
glass.add(:rye, amt: 0.75, measure: :oz)
glass.add(:brandy, amt: 0.75, measure: :oz)
glass.add(:sweet_vermouth, amt: 1, measure: :oz)
glass.add(:benedictine, amt: 0.5, measure: :oz)
glass.add(:angostura, amt: 2, measure: :dashes)
glass.add(:peychauds, amt: 1, measure: :dash)
glass.stir
end
end
Running the test again, we get another useful error message, albeit this time from Ruby and not Mocktail. We misspelled one of the keyword arguments:
1) Error:
VieuxCarreTest#test_mixes_up_a_lovely_drink:
ArgumentError: missing keyword: :measure [Mocktail call: `add(:cognac, amt: 0.75, mesaure: :oz)']
Fun fact: vim has a spellcheck function so if I place the cursor over mesaure
and type z=
, it will populate a list of suggestions and I can pick the first by typing 1
and it will fix my mistake.
When we re-run our tests, we can see another useful error message from Mocktail complaining that we’ve missed a step and showing us all of the calls that we did make to glass.add
:
1) Error:
VieuxCarreTest#test_mixes_up_a_lovely_drink:
Mocktail::VerificationError: Expected mocktail of `MixingGlass#add' to be called like:
add(:cognac, amt: 0.75, measure: :oz)
It was called differently 6 times:
add(:rye, amt: 0.75, measure: :oz)
add(:brandy, amt: 0.75, measure: :oz)
add(:sweet_vermouth, amt: 1, measure: :oz)
add(:benedictine, amt: 0.5, measure: :oz)
add(:angostura, amt: 2, measure: :dashes)
add(:peychauds, amt: 1, measure: :dash)
We can go ahead and correct our mistake, swapping our brandy for the correct cognac (sue me, my home bar doesn’t always have both of these). When we go again, Mocktail catches that I like my drinks on the sweet side and complains about the next mistake:
1) Error:
VieuxCarreTest#test_mixes_up_a_lovely_drink:
Mocktail::VerificationError: Expected mocktail of `MixingGlass#add' to be called like:
add(:sweet_vermouth, amt: 0.75, measure: :oz)
It was called differently 6 times:
add(:rye, amt: 0.75, measure: :oz)
add(:cognac, amt: 0.75, measure: :oz)
add(:sweet_vermouth, amt: 1, measure: :oz)
add(:benedictine, amt: 0.5, measure: :oz)
add(:angostura, amt: 2, measure: :dashes)
add(:peychauds, amt: 1, measure: :dash)
We’ll go ahead and undo all of our mistakes now that we’re sure Mocktail will catch us. Whatever happened to being creative with drinks?
class Bartender
def vieux_carre
glass = MixingGlass.new
glass.add(:rye, amt: 0.75, measure: :oz)
glass.add(:cognac, amt: 0.75, measure: :oz)
glass.add(:sweet_vermouth, amt: 0.75, measure: :oz)
glass.add(:benedictine, amt: 0.5, measure: :tsp)
glass.add(:angostura, amt: 1, measure: :dash)
glass.add(:peychauds, amt: 1, measure: :dash)
glass.stir
end
end
And indeed, our drink is yummy.
The great error messages, the fact that it provides really easy Class.new
interception, and the stupidly obvious stub {... }.with {...}
API are all solid reasons to give Mocktail a try in your projects. The argument validation we saw part of is also great, and we didn’t even go over that the stub/verify APIs also ensure that your arity is correct so you can’t accidentally stub out a call, change your method signature, and not catch that you’ve broken things.
We didn’t look into examples, but Mocktail also provides some really powerful argument matching (verify(times: 6) { glass.add(is_a(Symbol), amt: numeric, measure: any }
), singleton replacement for doubles of classes allowing for stubs and verifys while maintaining thread saftey, and a powerful argument verification API in the form of Mocktail.captor
.
Mocktail, unlike the most popular test double library rspec-mocks, isn’t tied to a single framework, so it meets you where you are regarding testing frameworks. It doesn’t monkey patch any of your objects, so you don’t need to worry about namespace collisions or other issues that can cause.
So, take a shot of Mocktail. I think you’ll like what you get.
Posted on August 31, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.