Mixing it up with Mocktail

calebhearth

Caleb Hearth

Posted on August 31, 2022

Mixing it up with Mocktail

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)']

Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
calebhearth
Caleb Hearth

Posted on August 31, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Mixing it up with Mocktail
programming Mixing it up with Mocktail

August 31, 2022