πΈ
Mocktail Mocktail is a test double library for Ruby that provides a terse and robust API for creating mocks, getting them in the hands of the code you're testing, stub & verify behavior, and even safely override class methods.
If you'd prefer a voice & video introduction to Mocktail aside from this README,
you might enjoy this
An aperitif
Before getting into the details, let's demonstrate what Mocktail's API looks
like. Suppose you want to test a Bartender
class:
class Bartender
def initialize
@shaker = Shaker.new
@glass = Glass.new
@bar = Bar.new
end
def make_drink(name, customer:)
if name == :negroni
drink = @shaker.combine(:gin, :campari, :sweet_vermouth)
@glass.pour!(drink)
@bar.pass(@glass, to: customer)
end
end
end
You could write an isolated unit test with Mocktail like this:
shaker = Mocktail.of_next(Shaker)
glass = Mocktail.of_next(Glass)
bar = Mocktail.of_next(Bar)
subject = Bartender.new
stubs { shaker.combine(:gin, :campari, :sweet_vermouth) }.with { :a_drink }
stubs { bar.pass(glass, to: "Eileen") }.with { "π" }
result = subject.make_drink(:negroni, customer: "Eileen")
assert_equal "π", result
# Oh yeah, and make sure the drink got poured! Silly side effects!
verify { glass.pour!(:a_drink) }
Why Mocktail?
Besides helping you avoid a hangover, Mocktail offers several advantages over Ruby's other mocking libraries:
- Simpler test recipes: Mocktail.of_next(type) both creates your mock and supplies to your subject under test in a single one-liner. No more forcing dependency injection for the sake of your tests
- WYSIWYG API: Want to know how to stub a call to
phone.dial(911)
? You just demonstrate the call withstubs { phone.dial(911) }.with { :operator }
. Because stubbing & verifying looks just like the actual call, your tests becomes a sounding board for your APIs as you invent them - Argument validation: Ever see a test pass after a change to a mocked method should have broken it? Not with Mocktail, you haven't
- Mocked class methods: Singleton methods on modules and classes can be
faked out using
Mocktail.replace(type)
without sacrificing thread safety - Super-duper detailed error messages A good mocking library should make coding feel like paint-by-number, thoughtfully guiding you from one step to the next. Calling a method that doesn't exist will print a sample definition you can copy-paste. Verification failures will print every call that did occur. And Mocktail.explain() provides even more introspection
- Expressive: Built-in argument matchers and a simple API for adding custom matchers allow you to tune your stubbing configuration and call verification to match exactly what your test intends
- Powerful: Argument captors for assertions of very complex arguments, as well as advanced configuration options for stubbing & verification
Ready to order?
Install the gem
The main ingredient to add to your Gemfile:
gem "mocktail", group: :test
Sprinkle in the DSL
Then, in each of your tests or in a test helper, you'll probably want to include
Mocktail's DSL. (This is optional, however, as every method in the DSL is also
available as a singleton method on Mocktail
.)
In Minitest, you might add the DSL with:
class Minitest::Test
include Mocktail::DSL
end
Or, in RSpec:
RSpec.configure do |config|
config.include Mocktail::DSL
end
Clean up when you're done
To reset Mocktail's internal state between tests and avoid test pollution, you
should also call Mocktail.reset
after each test:
In Minitest:
class Minitest::Test
# Or, if in a Rails test, in a `teardown doβ¦end` block
def teardown
Mocktail.reset
end
end
And RSpec:
RSpec.configure do |config|
config.after(:each) do
Mocktail.reset
end
end
API
The entire public API is listed in the top-level module's source. Below is a longer menu to explain what goes into each feature.
Mocktail.of
Mocktail.of(module_or_class)
takes a module or class and returns an instance
of an object with fake methods in place of all its instance methods which can
then be stubbed or verified.
class Clothes; end;
class Shoe < Clothes
def tie(laces)
end
end
shoe = Mocktail.of(Shoe)
shoe.instance_of?(Shoe) # => true
shoe.is_a?(Clothes) # => true
shoe.class == Shoe # => false!
shoe.to_s # => #<Mocktail of Shoe:0x00000001343b57b0>"
Mocktail.of_next
Mocktail.of_next(klass, [count: 1])
takes a class and returns one mock (the
default) or an array of multiple mocks. It also effectively overrides the
behavior of that class's constructor to return those mock(s) in order and
finally restoring its previous behavior.
For example, if you wanted to test the Notifier
class below:
class Notifier
def initialize
@mailer = Mailer.new
end
def notify(name)
@mailer.deliver!("Hello, #{name}")
end
end
You could write a test like this:
def test_notifier
mailer = Mocktail.of_next(Mailer)
subject = Notifier.new
subject.notify("Pants")
verify { mailer.deliver!("Hello, Pants") }
end
There's nothing wrong with creating mocks using Mocktail.of
and passing them
to your subject some other way, but this approach allows you to write very terse
isolation tests without foisting additional indirection or dependency injection
in for your tests' sake.
Mocktail.stubs
Configuring a fake method to take a certain action or return a particular value
is called "stubbing". To stub a call with a value, you can call Mocktail.stubs
(or just stubs
if you've included Mocktail::DSL
) and then specify an effect
that will be invoked whenever that call configuration is satisfied using with
.
The API is very simple in the simple case:
class UserRepository
def find(id, debug: false); end
def transaction(&blk); end
end
You could stub responses to a mock of the UserRepository
like this:
user_repository = Mocktail.of(UserRepository)
stubs { user_repository.find(42) }.with { :a_user }
user_repository.find(42) # => :a_user
user_repository.find(43) # => nil
user_repository.find # => ArgumentError: wrong number of arguments (given 0, expected 1)
The block passed to stubs
is called the "demonstration", because it represents
an example of the kind of calls that Mocktail should match.
If you want to get fancy, you can use matchers to make your demonstration more dynamic. For example, you could match any number with:
stubs { |m| user_repository.find(m.numeric) }.with { :another_user }
user_repository.find(41) # => :another_user
user_repository.find(42) # => :another_user
user_repository.find(43) # => :another_user
Stubbings are last-in-wins, which is why the stubbing above would have
overridden the earlier-but-more-specific stubbing of find(42)
.
A stubbing's effect can also be changed dynamically based on the actual call
that satisfied the demonstration by looking at the call
block argument:
stubs { |m| user_repository.find(m.is_a(Integer)) }.with { |call|
{id: call.args.first}
}
user_repository.find(41) # => {id: 41}
# Since 42.5 is a Float, the earlier stubbing will win here:
user_repository.find(42.5) # => :another_user
user_repository.find(43) # => {id: 43}
It's certainly more complex to think through, but if your stubbed method takes a block, your demonstration can pass a block of its own and inspect or invoke it:
stubs {
user_repository.transaction { |block| block.call == {id: 41} }
}.with { :successful_transaction }
user_repository.transaction {
user_repository.find(41)
} # => :successful_transaction
user_repository.transaction {
user_repository.find(40)
} # => nil
There are also several advanced options you can pass to stubs
to control its
behavior.
times
will limit the number of times a satisfied stubbing can have its effect:
stubs { |m| user_repository.find(m.any) }.with { :not_found }
stubs(times: 2) { |m| user_repository.find(1) }.with { :someone }
user_repository.find(1) # => :someone
user_repository.find(1) # => :someone
user_repository.find(1) # => :not_found
ignore_extra_args
will allow a demonstration to be considered satisfied even
if it fails to specify arguments and keyword arguments made by the actual call:
stubs { user_repository.find(4) }.with { :a_person }
user_repository.find(4, debug: true) # => nil
stubs(ignore_extra_args: true) { user_repository.find(4) }.with { :b_person }
user_repository.find(4, debug: true) # => :b_person
And ignore_block
will similarly allow a demonstration to not concern itself
with whether an actual call passed the method a blockβit's satisfied either way:
stubs { user_repository.transaction }.with { :transaction }
user_repository.transaction {} # => nil
stubs(ignore_block: true) { user_repository.transaction }.with { :transaction }
user_repository.transaction {} # => :transaction
Mocktail.verify
In practice, we've found that we stub far more responses than we explicitly
verify a particular call took place. That's because our code normally returns
some observable value that is influenced by our dependencies' behavior, so
adding additional assertions that they be called would be redundant. That
said, for cases where a dependency doesn't return a value but just has a
necessary side effect, the verify
method exists (and like stubs
is included
in Mocktail::DSL
).
Once you've gotten the hang of stubbing, you'll find that the verify
method is
intentionally very similar. They almost rhyme.
For this example, consider an Auditor
class that our code might need to call
to record that certain actions took place.
class Auditor
def record!(message, user_id:, action: nil); end
end
Once you've created a mock of the Auditor
, you can start verifying basic
calls:
auditor = Mocktail.of(Auditor)
verify { auditor.record!("hello", user_id: 42) }
# => raised Mocktail::VerificationError
# Expected mocktail of Auditor#record! to be called like:
#
# record!("hello", user_id: 42)
#
# But it was never called.
Wups! Verify will blow up whenever a matching call hasn't occurred, so it should be called after you've invoked your subject under test along with any other assertions you have.
If we make a call that satisfies the verify
call's demonstration, however, you
won't see that error:
auditor.record!("hello", user_id: 42)
verify { auditor.record!("hello", user_id: 42) } # => nil
There, nothing happened! Just like any other assertion library, you only hear
from verify
when verification fails.
Just like with stubs
, you can any built-in or custom matchers can serve as
garnishes for your demonstration:
auditor.record!("hello", user_id: 42)
verify { |m| auditor.record!(m.is_a(String), user_id: m.numeric) } # => nil
# But this will raise a VerificationError:
verify { |m| auditor.record!(m.is_a(String), user_id: m.that { |arg| arg > 50}) }
When you pass a block to your demonstration, it will be invoked with any block that was passed to the actual call to the mock. Truthy responses will satisfy the verification and falsey ones will fail:
auditor.record!("ok", user_id: 1) { Time.new }
verify { |m| auditor.record!("ok", user_id: 1) { |block| block.call.is_a?(Time) } } # => nil
# But this will raise a VerificationError:
verify { |m| auditor.record!("ok", user_id: 1) { |block| block.call.is_a?(Date) } }
verify
supports the same options as stubs
:
times
will require the demonstrated call happened exactlytimes
times (by default, the call has to happen 1 or more times)ignore_extra_args
will allow the demonstration to forego specifying optional arguments while still being considered satisfiedignore_block
will similarly allow the demonstration to forego specifying a block, even if the actual call receives one
Note that if you want to verify a method wasn't called at all or called a specific number of timesβespecially if you don't care about the parameters, you may want to look at the Mocktail.calls() API.
Mocktail.matchers
You'll probably never need to call Mocktail.matchers
directly, because it's
the object that is passed to every demonstration block passed to stubs
and
verify
. By default, a stubbing (e.g. stubs { email.send("text") }
) is only
considered satisfied if every argument passed to an actual call was passed an
==
check. Matchers allow us to relax or change that constraint for both
regular arguments and keyword arguments so that our demonstrations can match
more kinds of method invocations.
Matchers allow you to specify stubbings and verifications that look like this:
stubs { |m| email.send(m.is_a(String)) }.with { "I'm an email" }
Built-in matchers
These matchers come out of the box:
any
- Will match any value (even nil) in the given argument position or keywordis_a(type)
- Will match when itstype
passes anis_a?
check against the actual argumentincludes(thing, [**more_things])
- Will match when all of its arguments are contained by the corresponding argumentβbe it a string, array, hash, or anything that responds toincludes?
matches(pattern)
- Will match when the provided string or pattern passes amatch?
test on the corresponding argument; usually used to match strings that contain a particular substring or pattern, but will work with any argument that responds tomatch?
not(thing)
- Will only match when its argument does not equal (via!=
) the actual argumentnumeric
- Will match when the actual argument is an instance ofInteger
,Float
, or (if loaded)BigDecimal
that { |arg| β¦ }
- Takes a block that will receive the actual argument. If the block returns truthy, it's considered a match; otherwise, it's not a match.
Custom matchers
If you want to write your own matchers, check out the source for
examples. Once you've implemented a class,
just pass it to Mocktail.register_matcher
in your test helper.
class MyAwesomeMatcher < Mocktail::Matchers::Base
def self.matcher_name
:awesome
end
def match?(actual)
"#{@expected}β¨" == actual
end
end
Mocktail.register_matcher(MyAwesomeMatcher)
Then, a stubbing like this:
stubs { |m| user_repository.find(m.awesome(11)) }.with { :awesome_user }
user_repository.find("11")) # => nil
user_repository.find("11β¨")) # => :awesome_user
Mocktail.captor
An argument captor is a special kind of matcher⦠really, it's a matcher factory.
Suppose you have a verify
call for which one of the expected arguments is
really complicated. Since verify
tends to be paired with fire-and-forget
APIs that are being invoked for the side effect, this is a pretty common case.
You want to be able to effectively snag that value and then run any number of
specific assertions against it.
That's what Mocktail.captor
is for. It's easiest to make sense of this by
example. Given this BigApi
class that's presumably being called by your
subject at the end of a lot of other work building up a payload:
class BigApi
def send(payload); end
end
You could capture the value of that payload as part of the verification of the call:
big_api = Mocktail.of(BigApi)
big_api.send({imagine: "that", this: "is", a: "huge", object: "!"})
payload_captor = Mocktail.captor
verify { big_api.send(payload_captor.capture) } # => nil!
The verify
above will pass because a call did happen, but we haven't
asserted anything beyond that yet. What really happened is that
payload_captor.capture
actually returned a matcher that will return true for
any argument while also sneakily storing a copy of the argument value.
That's why we instantiated payload_captor
with Mocktail.captor
outside the
demonstration block, so we can inspect its value
after the verify
call:
payload_captor = Mocktail.captor
verify { big_api.send(payload_captor.capture) } # => nil!
payload = payload_captor.value # {:imagine=>"that", :this=>"is", :a=>"huge", :object=>"!"}
assert_equal "huge", payload[:a]
Mocktail.replace
Mocktail was written to support isolated test-driven development, which usually results in a lot of boring classes and instance methods. But sometimes you need to mock singleton methods on classes or modules, and we support that too.
When you call Mocktail.replace(type)
, all of the singleton methods on the
provided type are replaced with fake methods available for stubbing and
verification. It's really that simple.
For example, if our Bartender
class has a class method:
class Bartender
def self.cliche_greeting
["It's 5 o'clock somewhere!", "Norm!"].sample
end
end
We can replace the behavior of the overall class, and then stub how we'd like it to respond, in our test:
Mocktail.replace(Bartender)
stubs { Bartender.cliche_greeting }.with { "Norm!" }
[Obligatory warning: Mocktail does its best to ensure that other threads won't be affected when you replace the singleton methods on a type, but your mileage may very! Singleton methods are global and code that introspects or invokes a replaced method in a peculiar-enough way could lead to hard-to-track down bugs. (If this concerns you, then the fact that class methods are effectively global state may be a great reason not to rely too heavily on them!)]
Mocktail.explain
Test debugging is hard enough when there aren't fake objects flying every
which way, so Mocktail tries to make it a little easier on you. In addition to
returning useful messages throughout the API, the gem also includes an
introspection method Mocktail.explain(thing)
, which returns a human-readable
message
and a reference
object with useful attributes (that vary depending
on the type of fake thing
you pass in. Below are some things explain()
can
do.
Fake instances created by Mocktail
Any instances created by Mocktail.of
or Mocktail.of_next
can be passed to
Mocktail.explain
, and they will list out all the calls and stubbings made for
each of their fake methods.
Suppose these interactions have occurred:
ice_tray = Mocktail.of(IceTray)
Mocktail.stubs { ice_tray.fill(:tap_water, 30) }.with { :some_ice }
ice_tray.fill(:tap_water, 50)
You can interrogate what's going on with the fake instance by passing it to
explain
:
explanation = Mocktail.explain(ice_tray)
explanation.reference.type #=> IceTray
explanation.reference.double #=> The ice_tray instance
explanation.reference.calls #=> details on each invocation of each method
explanation.reference.stubbings #=> all stubbings configured for each method
Calling explanation.message
will return:
This is a fake `IceTray' instance.
It has these mocked methods:
- fill
`IceTray#fill' stubbings:
fill(:tap_water, 30)
`IceTray#fill' calls:
fill(:tap_water, 50)
Modules and classes with singleton methods replaced
If you've called Mocktail.replace()
on a class or module, it can also be
passed to Mocktail.explain()
for a summary of all the stubbing configurations
and calls made against its faked singleton methods for the currently running
thread.
Imagine a Shop
class with self.open!
and self.close!
singleton methods:
Mocktail.replace(Shop)
stubs { |m| Shop.open!(m.numeric) }.with { :a_bar }
Shop.open!(42)
Shop.close!(42)
explanation = Mocktail.explain(Shop)
explanation.reference.type #=> Shop
explanation.reference.replaced_method_names #=> [:close!, :open!]
explanation.reference.calls #=> details on each invocation of each method
explanation.reference.stubbings #=> all stubbings configured for each method
And explanation.message
will return:
`Shop' is a class that has had its singleton methods faked.
It has these mocked methods:
- close!
- open!
`Shop.close!' has no stubbings.
`Shop.close!' calls:
close!(42)
close!(42)
`Shop.open!' stubbings:
open!(numeric)
open!(numeric)
`Shop.open!' calls:
open!(42)
open!(42)
Methods on faked instances and replaced types
In addition to passing the test double, you can also pass a reference to any
fake method created by Mocktail to Mocktail.explain
:
ice_tray = Mocktail.of(IceTray)
ice_tray.fill(:chilled, 50)
explanation = Mocktail.explain(ice_tray.method(:fill))
explanation.reference.receiver #=> a reference to the `ice_tray` instance
explanation.reference.calls #=> details on each invocation of the method
explanation.reference.stubbings #=> all stubbings configured for the method
The above may be handy in cases where you want to assert the number of calls of
a method outside the Mocktail.verify
API:
assert_equal 1, explanation.reference.calls.size
The explanation will also contain a message
like this:
`IceTray#fill' has no stubbings.
`IceTray#fill' calls:
fill(:chilled, 50)
Replaced singleton methods can also be passed to explain()
, so something like
Mocktail.explain(Shop.method(:open!))
from the earlier example would also work
(with Shop
being the receiver
on the explanation's reference
).
Undefined methods
There's no API for this one, but Mocktail also offers explanations for methods that don't exist yet. You'll see this error message whenever you try to call a method that doesn't exist on a test double. The message is designed to facilitate "paint-by-numbers" TDD, by including a sample definition of the method you had attempted to call that can be copy-pasted into a source listing:
class IceTray
end
ice_tray = Mocktail.of(IceTray)
ice_tray.fill(:water_type, 30)
# => No method `IceTray#fill' exists for call: (NoMethodError)
#
# fill(:water_type, 30)
#
# Need to define the method? Here's a sample definition:
#
# def fill(water_type, arg)
# end
From there, you can just copy-paste the provided method stub as a starting point for your new method:
class IceTray
def fill(water_type, amount)
end
end
Mocktail.explain_nils
Is a faked method returning nil
and you don't understand why?
By default, methods faked by Mocktail will return nil
when no stubbing is
satisfied. A frequent frustration, therefore, is when the way stubs {}.with {}
is configured does not satisfy a call the way you expected. To try to make
debugging this a little bit easier, the gem provides a top-level
Mocktail.explain_nils
method that will return an array of summaries of every
call to a faked method that failed to satisfy any stubbings.
For example, suppose you stub this fill
method like so:
ice_tray = Mocktail.of(IceTray)
stubs { ice_tray.fill(:tap_water, 30) }.with { :normal_ice }
But then you find that your subject under test is just getting nil
back and
you don't understand why:
def prep
ice = ice_tray.fill(:tap_water, 50)
glass.add(ice) # => why is `ice` nil?!
end
Whenever you're confused by a nil, you can call Mocktail.explain_nils
for an
array containing UnsatisfyingCallExplanation
objects (one for each call to
a faked method that did not satisfy any configured stubbings).
The returned explanation objects will include both a reference
object to
explore as well a summary message
:
def prep
ice = ice_tray.fill(:tap_water, 50)
puts Mocktail.explain_nils.first.message
glass.add(ice)
end
Which will print:
This `nil' was returned by a mocked `IceTray#fill' method
because none of its configured stubbings were satisfied.
The actual call:
fill(:tap_water, 50)
The call site:
/path/to/your/code.rb:42:in `prep'
Stubbings configured prior to this call but not satisfied by it:
fill(:tap_water, 30)
The reference
object will have details of the call
itself, an array of
other_stubbings
defined on the faked method, and a backtrace
to determine
which call site produced the unexpected nil
value.
Mocktail.calls
When practicing test-driven development, you may want to ensure that a
dependency wasn't called at all. To provide a terse way to express this,
Mocktail offers a top-level calls(double, method_name = nil)
method that
returns an array of the calls to the mock (optionally filtered to a
particular method name) in the order they were called.
Suppose you were writing a test of this method for example:
def import_users
users_response = @gets_users.get
if users_response.success?
@upserts_users.upsert(users_response.data)
end
end
A test case of the negative branch of that if
statement (when success?
is
false) might simply want to assert that @upserts_users.upsert
wasn't called at
all, regardless of its parameters.
The easiest way to do this is to use Mocktail.calls()
method, which is an
alias of Mocktail.explain(double).reference.calls that can
filter to a specific method name. In the case of a test of the above method, you
could assert:
# Assert that the `upsert` method on the mock was never called
assert_equal 0, Mocktail.calls(@upserts_users, :upsert).size
# Assert that NO METHODS on the mock were called at all:
assert_equal 0, Mocktail.calls(@upserts_users).size
If you're interested in doing more complicated introspection in the nature of
the calls, their ordering, and so forth, the calls
method will return
Mocktail::Call
values with the args, kwargs, block, and information about the
original class and method being mocked.
(While this behavior can technically be accomplished with verify(times: 0) { β¦ }
, it's verbose and error prone to do so. Because verify
is careful to only
assert exact argument matches, it can get pretty confusing to remember to tack
on ignore_extra_args: true
and to call the method with zero args to cover all
cases.)
Mocktail.reset
This one's simple: you probably want to call Mocktail.reset
after each test,
but you definitely want to call it if you're using Mocktail.replace
or
Mocktail.of_next
anywhere, since those will affect state that is shared across
tests.
Calling reset in a teardown
or after(:each)
hook will also improve the
usefulness of messages returned by Mocktail.explain
and
Mocktail.explain_nils
.
References
Mocktail is designed following a somewhat academic understanding of what mocking is and how it should be used. Below are several references on this topic.
Blog Posts and Papers:
- [Endo-Testing: Unit Testing with Mock Objects](https://www2.ccs.neu.edu/research/demeter/related-work/extreme-programming/MockObjectsFinal.PDF by Tim Mackinnon, Steve Freeman, and Philip Craig, the paper that introduced mocking presented by the creators of mocking.
- Michael Feathers' The Flawed Theory Behind Unit Testing
Books:
- Growing Object-Oriented Software, Guided by Tests by Steve Freeman and Nat Price
Talks:
- Please donβt mock me by Justin Searls
Acknowledgements
Mocktail is created & maintained by the software agency Test Double. If you've ever come across our eponymously-named testdouble.js, you might find Mocktail's API to be quite similar. The term "test double" was originally coined by Gerard Meszaros in his book xUnit Test Patterns.
The name is inspired by the innovative Java mocking library Mockito. Mocktail also the spiritual successor to gimme, which offers a similar API but which fell victim to the limitations of Ruby 1.8.7 (and @searls's Ruby chops). Gimme was also one of the final projects we collaborated with Jim Weirich on, so this approach to isolated unit testing holds a special significance to us.
Code of Conduct
This project follows Test Double's code of conduct for all community interactions, including (but not limited to) one-on-one communications, public posts/comments, code reviews, pull requests, and GitHub issues. If violations occur, Test Double will take any action they deem appropriate for the infraction, up to and including blocking a user from the organization's repositories.