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

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.

Webmentions

Dave Copeland

@caleb @searls The domain of making a cocktail is extremely confusing to me. It's so far from any code anyone would write I'm getting lost while also have to learn mocktail.

I also get extremely confused by teaching new libs through TDD - there's a bunch of unknown stuff in the code and half is mocktail and half is yet-to-be-written code, and I can't tell which is which.

Here is how I might explain Rspec's mocking to avoid these issues: gist.github.com/davetron5000/c

rspec_mocks_quickie.md
Peter Solnica

@davetron5000 @noelrap actually, rspec is *too good* at mocking 😆 I mean, it can be very helpful in some cases but I have a feeling that overall it causes damage. For me, even doing stuff like mocking `new` to return an instance double is already too much. In general, I tend to mock very rarely these days. Not to mention that people *notoriously* mock interfaces that they don't own, it's especially fun when they don't use verified doubles 😭

Bradley Schaefer

@davetron5000 @solnic @noelrap I hesitate to connect it to DI (but I get it) - I would challenge the idea that it's only for testing.

One example I can think of is usages of Time.now - passing in Time or Time.now lets you test easily without time travel, sure, but it also opens the capability of backdating that operation. That's something that comes up in practice repeatedly.

Once you have a lot of collaborators it sucks. We don't live in a perfect world!

Nathan Ladd

@soulcutter @davetron5000 @solnic @noelrap

It seems like the principal objection to allowing the clock (Time) to be specified by the user is that *forcing* a user to supply a clock when they didn't need to before clearly harms usability.

This seems indisputable, but *allowing* the user to override the clock with something they can control only seems to increase usability. I can't figure out what the pro-mock objection to optional dependency substitution is.

Jared Norman

@soulcutter @davetron5000 @solnic @noelrap The London-style approach has most objects taking in their dependencies (either through constructor or method arguments depending), and then some layer of the application is concerned with composing those objects.

The benefit is that those collaborators can be swapped, ideally without even updating tests.

If you're stubbing new to return mocks, you've hardcoded your dependencies in both your source and tests, going against context independence.

Dave Copeland

@jared @soulcutter @solnic @noelrap I like code to reflect reality - if a dependency is explicit, hiding it being DI/interfaces doesn't help anything. But in Java, you have to do this in order to mock Bar in a test. Ruby does not have this constraint. Explicitly injecting dependencies is only useful in Ruby in two occasions: 1) there are multiple implementations that might be used, 2) creating the dependency is complicated and had to be done elsewhere. IME these are rare.

Bradley Schaefer

@davetron5000 @ntl @solnic @noelrap YAGNI, until you do, and find out how challenging it is to change.

I do buy the YAGNI POV, and I also think this is a tough topic to turn into hard rules.

E.g. the Time thing has been handy to me in my experience, so I tend to do it. I don't tend to do that everywhere, though, so there's some heuristic I have there.

Nathan Ladd

@soulcutter @davetron5000 @solnic @noelrap

I think there's a problem with applying YAGNI here.

A class doesn't strictly *need* to have, say observability, so why not prohibit logging and telemetry? Arguably, implementations are typically enriched enough by observability to justify the elaboration.

Controllability is in the same boat, in my experience.

Sure, I can always modify the runtime indirectly to control a dependency. But why?

My car's purpose is transportation, but it's got a hood.

Dave Copeland

@ntl @soulcutter @solnic @noelrap But the system needs those things, so the class may need to provide the system that capability since there may not be any other way.

The system (or dev environment) does not need Time to be injectible if the only reason to do so is test that Time.now was called.

In Java, the dev environment DOES need things to be injectible because it needs to be able to mock inside tests. For the dev environment to do that, classes must accommodate.

Nathan Ladd

@soulcutter @davetron5000 @solnic @noelrap

I found myself agreeing with many of David's points in the gist he linked, though in practice I think those points lead me to a different conclusion.

In particular, being unable to spot the concrete class of a dependency is one of the tell tale signs of dependency management gone wrong. Reminds me of angular.js (where the hell are all these function variables actually *coming* from? Narnia?)

Dave Copeland

@ntl @soulcutter @solnic @noelrap From Javaland, DI was basically "you should never ever call new" and the result of a system built that way is that it is harder to understand than it needs to be.

If we mean something else by DI, might be good to clarify as I'm not sure what should or should not be injected as a dependency (barring the need for a dependency to be one of many possible implementations).

Nathan Ladd

@davetron5000 @soulcutter @solnic @noelrap

I think I see what you mean.

Sometimes, all that's needed for a test to verify is that a time value was set on something. Other times, a time value must be verified precisely. The two scenarios would require the same approach in Java, but in Ruby, they are allowed to be different.

Nathan Ladd

@davetron5000 @soulcutter @solnic @noelrap

My experience is that I almost always need dependencies to be one of a number of possible implementations. Usually, that number is two. In the case of a clock, I usually either need a clock that produces the system clock time, or else I need a clock whose notion of current time can be controlled directly.

But "need" is an unhelpful word to use here -- to me, the need is real, but to others, y'all might say "'just' use mocks."

Nathan Ladd

@davetron5000 @soulcutter @solnic @noelrap

Ultimately, I'm left with a value judgement that I can't reduce -- objects *should* have affordances for controlling their dependencies, because those interfaces are manifestly useful.

I draw a distinction between making an object more "testable" (i.e. amenable to test automation.. not helpful), and making an object more observable and controllable -- which *always* makes the object more testable as a byproduct. After all, tests only control & observe

Dave Copeland

@ntl @soulcutter @solnic @noelrap Yeah, could be we just disagree on some core value - I do not want objects to be fungible in general and do not design them to have their dependencies controlled externally, at least not be default. From my view it is extra work that I can't find a justification for and adds complexity to the system.

Nathan Ladd

@davetron5000 @soulcutter @solnic @noelrap

Ah. It seems like you value interchangeability only when you need versatility (e.g. "plugging in" different payment processors).

I always aim to milk interchangeability for all its worth, which means I typically need to be talked in to not having it. In light of the benefits I leverage regularly, I find the marginal cost to be easily justified.

I wish it were easier to communicate *how* we work, vs. what the tangible work product looks like.