While the entire public API is listed in the top-level module's source listing, but here are some more in-depth descriptions of all of Mocktail's public methods.
- Creating mocks
- Stubbing & verifying interactions
- Matching Arguments dynamically
- Mocking class and module methods
- Debugging
- Managing internal state
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(SomeClass)
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.
In addition to telling Mocktail to overwrite new
on a target Class to return
exactly one fake object before reverting to its original new
method, you can
generate a set of fake objects this way.
class Dice
def roll
(1..6).to_a.sample
end
end
loaded_dice = Mocktail.of_next_with_count(Dice, 2)
stubs { loaded_dice[0].roll }.with { 1 }
stubs { loaded_dice[1].roll }.with { 1 }
# Then, over in your subject under test:
puts [Dice.new, Dice.new].sum { |dice| dice.roll }
#=> 2 # guaranteed snake eyes 🎲🎲
Dice.new.roll # Back to random rolls!
Mocktail.of_next(Dice, count: 2)
would also work in the above case, but isn't
able to be type-checked by Sorbet (because it can't change the return type to an
Array based on a non-1
value of the count
argument), so unless you
disable Sorbet's runtime type checking,
this will raise an exception.
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
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.
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" }
These matchers come out of the box:
m.any
- Will match any value (even nil) in the given argument position or
keyword
m.is_a(type)
- Will match when its type
passes an is_a?
check against the
actual argument
m.includes(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 to includes?
m.matches(pattern)
- Will match when the provided string or pattern passes a
match?
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 to match?
m.not(thing)
- Will only match when its argument does not equal (via !=
)
the actual argument
m.numeric
- Will match when the actual argument is an instance of Integer
,
Float
, or (if loaded) BigDecimal
m.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.
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
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 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 methods on classes or modules, and we support that too.
When you call Mocktail.replace(type)
, all of the methods defined 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 globally-referenceable 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!)]
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.
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)
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 methods for the currently running thread.
Imagine a Shop
class with self.open!
and self.close!
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 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)
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 class 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
).
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
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.
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.)
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
.