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
@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: https://gist.github.com/davetron5000/c7d22be3edc991c2eab32190b8783fc0
rspec_mocks_quickie.md@caleb @searls I'm trying to write a "how would do this mocking in minitest" but lol I cannot figure out how to do it in minitest
@davetron5000 thanks! @caleb are you down for trying to transliterate this gist into minitest/mocktail?
@searls @davetron5000 I can do that, sure.
@davetron5000 This is great! I do default to spies rather than doubles, but regardless that explanation is <chef kiss>
@davetron5000 I think this is traditionally where I give up on mini test any time I try it..
@noelrap I have used it before but cannot remember how and definitely remember feeling like rspec's is superior to it
@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 😭
@solnic @davetron5000 @noelrap In an ideal world, I would never mock `new` but rather pass collaborating objects in (to the method or constructor).
The `new` thing comes up a lot in practice, tho
@soulcutter @solnic @noelrap Ha, I would always mock new and never do DI just for testing. DI-just-for-testing is giving your code features it doesn't need ( at least in Ruby ). The only reason DI exists is because Java doesn't let you mock new.
@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!
@davetron5000 @soulcutter @noelrap DI is great for object composition and ease of testing is a nice side-effect. Java DI is irrelevant here. It’s sooo sad that people still connect DI with Java and claim it’s not a good design practice in Ruby.
@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.
@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.
@soulcutter @davetron5000 @solnic @noelrap That said (since this conversation mentions it a ways back) Mocktail is very much not designed to complement London-style (aka Mockist) TDD. Definitely a different style of mocking going on there.
@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.
@ntl @soulcutter @solnic @noelrap https://gist.github.com/davetron5000/9997e9d90c9076a8f8c528303ab5e502
If a class does not need a feature, that class should not have that feature. In Ruby, in most cases, you do not need to make time a parameter. If the classes must allow overriding the clock, great! it should. but if it does not require that, why add it?
mocking_time.md@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.
@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.
@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.
@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?)
@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).
@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.
@ntl @davetron5000 @solnic @noelrap I feel like I really hijacked this thread. Apologies!
I have a lot of opinions on testing 😂
@soulcutter @ntl @solnic @noelrap Here is what I do for managing dependencies between classes (keeping in mind I don't put logic in active records): https://github.com/sustainable-rails/uses
it doesn't create injectible dependencies but reduces friction in the code and the tests.
GitHub - sustainable-rails/uses: A minimal highly useful library for managing a service layer@davetron5000 @soulcutter @solnic @noelrap Ah, that’s a solid point. I think you’re losing out on some design benefits by going that route and I’m not sure I’m on board, but your explanation makes this design approach make way more sense to me.
@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."
@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
@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.
@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.
@davetron5000 I do like that uses hoists the dependency to a clearly visible place and adds useful behavior like circular checking.
I would write
def payment_processor
@payment_processor ||= PaymentProcessor.new
end
As a local variable. It drives me nuts how often I see people do it for no reason. It’s a ruby version of enterprise programming.
https://www.soulcutter.com/articles/local-variable-aversion-antipattern.html
The Local Variable Aversion Antipattern