Skip to content

Latest commit

Β 

History

History
515 lines (396 loc) Β· 16.9 KB

stubbing_and_verifying.md

File metadata and controls

515 lines (396 loc) Β· 16.9 KB

Stubbing and verifying mocked methods

The test doubles created by Mocktail can be used to aid in the setup and assertion of isolated unit tests by offering stub configuration and spy verification, respectively.

A headline benefit of choosing Mocktail over other mocking libraries is that once you know how to stub a method, you also know how to verify a method. Because Mocktail's Mocktail.stubs and Mocktail.verify methods are the two most-used methods in the library, and because they both enable variations of the same behaviorβ€”stubs anticipates future calls to a dependency whereas verify ensures past calls occurredβ€”their APIs are similarly symmetrical. That means both the stubs and verify methods share the same basic signature and all the same options. Mocking is poorly understood by a lot of developers, so we took a lot of care in designing an API that reflected their conceptual similarity instead of forcing users to memorize a larger API surface.

Stubbing behavior

When you've mocked out a dependency of the subject you're testing, you can use Mocktail's stubs DSL method in your tests to configure its methods to respond based on the arguments and blocks they're passed.

In these examples, we'll look at an example dependency object with a few instance methods we might want to stub.

class Bartop
  def place_coaster(seat_position = 0)
    # …
  end

  def clean_surface(with:)
    # …
  end
end

And we'll work with a mock instance we can create with Mocktail.of:

bartop = Mocktail.of(Bartop)

Initially, bartop will return nil for any invocation of its faked methods, but will still require arguments match their specified signature, raising ArgumentError if they aren't provided:

> bartop.clean_surface(with: :rag)
=> nil
> bartop.clean_surface
=> missing keyword: :with [Mocktail call: `clean_surface'] (ArgumentError)

Simple, no-arg stubbing

Because place_coaster's only parameter has a default value, the simplest stubbing we can create is the no-arg case:

stubs { bartop.place_coaster }.with { :a_coaster }

From then onward, calling the method without args will return :a_coaster:

> bartop.place_coaster
=> :a_coaster
> bartop.place_coaster(1)
=> nil
> bartop.place_coaster()
=> :a_coaster

We can also stub the same method multiple times. Newer stubbings will override older ones, as configured stubbings are matched against invocations on a "last-in wins" basis:

> stubs { bartop.place_coaster }.with { :a_napkin }
=> nil
> bartop.place_coaster
=> :a_napkin

You can also limit the number of times a stubbing can be satisfied by providing a times keyword argument to stubs:

> stubs(times: 2) { bartop.place_coaster }.with { :gold_leaf }
=> nil
> bartop.place_coaster
=> :gold_leaf
> bartop.place_coaster
=> :gold_leaf
> bartop.place_coaster
=> :a_napkin

As you can see above, as soon as the :gold_leaf stubbing hit its satisfaction limit of 2, place_coaster started once again responding with :a_napkin.

Stubbing with arguments

Of course, you wouldn't need a library if all you were stubbing was no-arg methods, so let's start passing some values:

stubs { bartop.place_coaster(1) }.with { :coaster_1 }
stubs { bartop.place_coaster(2) }.with { :coaster_2 }

And you can probably guess how these will behave:

> bartop.place_coaster(2)
=> :coaster_2
> bartop.place_coaster(1)
=> :coaster_1
> bartop.place_coaster(3)
=> nil

Keyword arguments work the same way as positional arguments:

stubs { bartop.clean_surface(with: :bleach) }.with { "πŸ‘ƒ" }
stubs { bartop.clean_surface(with: :rag) }.with { "✨" }

> bartop.clean_surface(with: :rag)
=> "✨"
> bartop.clean_surface(with: :bleach)
=> "πŸ‘ƒ"
> bartop.clean_surface(with: :toothbrush)
=> nil

Stubbing with inexact, dynamic arguments

When fully-isolated, tests will often provide exactly the values that the subject will receive at every step, and therefore will be able to provide a demonstration to stubs that passes the exact arguments passed by the subject, or at least expected arguments that will pass an equality check with the actual ones used by the subject.

But in more complex cases, you may need to configure a stubbing based on a dynamic description of the arguments. Mocktail enables this with argument matchers.

Here's a contrived example of Mocktail's built-in matcher API. A subject might pass a random value to a dependency, which would definitely make it difficult for a test to know the exact value being passed. Matchers could be used to configure whether a stubbing or verification is satisfied.

Given this subject:

def leave_bathroom
  @wash_hands.for_seconds(rand(5..10))
end

A stubbing of for_seconds could work around the randomness by just matching any value using m.any:

stubs { |m| @wash_hands.for_seconds(m.any) }.with { :small_suds }

> @wash_hands.for_seconds(3)
=> :small_suds

Or it could enforce the type with m.numeric:

stubs { |m| @wash_hands.for_seconds(m.numeric) }.with { :medium_suds }

> @wash_hands.for_seconds(30)
=> :medium_suds
> @wash_hands.for_seconds("some time")
=> nil

Or, to be even more precise, a matcher like m.thatβ€”which takes a block param validate the each argument by itself being invoked

stubs { |m|
  @wash_hands.for_seconds(m.that {|s| s.between?(5, 10) })
}.with { :big_suds }

> @wash_hands.for_seconds(7)
=> :big_suds
> @wash_hands.for_seconds(1)
=> nil
> @wash_hands.for_seconds(14)
=> nil

For more on the various matchers that ship with Mocktail as well as how to create your own custom matchers, check out their API documentation.

There is a lot more you can do with the Mocktail.stubs method, but the basics shown abouve should cover the vast majority of usage.

Verifying behavior

As mentioned at the top, Mocktail's mocks work as spies, allowing users to verify that the subject invoked a method as expected. Mocktail exposes this behavior through its verify DSL method. This section assumes you read and understand the stubs section above, as the API is largely the same.

Before we dive in, there's a worthwhile discussion to be had comparing the merits of using stubs and verify, because they weren't created equal.

Pure functions, those who return the same value for the same inputs and have no side effects, confer a lot of benefits to developers: easier to comprehend, easier to compose, and easier to test. It's generally worth striving to minimize the number of side effects scattered throughout a codebase, but modern programming languages and frameworks often make it very easy to write side-effect heavy code by failing to provide meaningful return values, especially when I/O is concerned. Practicing test-driven development with mocks, however, shines a bright light on side effects in your dependencies: each time you call verify, you're introducing a side effect into your code.

As a result, it's possible (and in a sense, laudable) to only occasionally reach for Mocktail's verify method. That said, Ruby doesn't lend itself especially well to purely functional designs and, regardless, some number of side effects are unavoidable for systems that interact with the outside world. And because side effects are often very difficult to test (given the lack of a return value), mocking libraries can make it very easy to test an interaction happens as intended.

Suppose you have a subject that needs to call a dependency that has a side effect and no return value (be wary of APIs that do both, violating command-query separation).

Let's make up an example of such a dependency:

class OrdersLimes
  def order!(lime_count = 1, shipping: :overnight)
    # …
  end
end

orders_limes = Mocktail.of(OrdersLimes)

Verifying a no-arg interaction

The simplest verification a test can make is of a dependent method with no arguments. We can verify that order! was invoked like this:

verify { orders_limes.order! }

But it hasn't been called yet! So verify will raise a Mocktail::VerificationError:

Expected mocktail of `OrdersLimes#order!' to be called like: (Mocktail::VerificationError)

  order!

But it was never called.

What if we try again? This time calling order! first:

> orders_limes.order!
=> nil
> verify { orders_limes.order! }
=> nil

Nothing happened! Just as you'd expect. The verification passed so no action is necessary and the test can proceed.

We can call order! an arbitrary number of times and verify it as many times as we like. By default, verify only cares that the specified interaction occurred at least once.

Verifying methods with arguments

When verifying an invocation with arguments, the same rules apply as for stubbing: each actual positional and keyword argument is compared with those specified in the verify demonstration using == or, optionally, an argument matchers.

Let's call order! a few times in different ways:

orders_limes.order!(3)
orders_limes.order!(50, shipping: :two_day)
orders_limes.order!(shipping: :ground)

Now let's try a verification that we know will fail:

verify { orders_limes.order!(4, shipping: :ground) }

This will fail as we'd expect, as well as printing out summaries of the prior invocations:

Expected mocktail of `OrdersLimes#order!' to be called like: (Mocktail::VerificationError)

  order!(4, shipping: :ground)

It was called differently 3 times:

  order!(3)

  order!(50, shipping: :two_day)

  order!(shipping: :ground)

Mocktail does its best to reconstruct a scrutible string for each invocation to ease in debugging unexpected failures, but if that's enough, you can also leverage its Mocktail.calls method to inspect each invocation to order!, replete with references to each argument passed:

> Mocktail.calls(orders_limes, :order!)
=>
[#<Mocktail::Call:0x0000000104631af0
  @args=[3],
  @block=nil,
  @double=#<Mocktail of OrdersLimes:0x00000001044974b0>,
  @dry_type=#<Class for mocktail of OrdersLimes:0x000000010465e758>,
  @kwargs={},
  @method=:order!,
  @original_method=#<UnboundMethod: OrdersLimes#order!(lime_count=..., shipping: ...),
  @original_type=OrdersLimes,
  @singleton=false>,
 #<Mocktail::Call:0x0000000104652318
  @args=[50],
  @block=nil,
  @double=#<Mocktail of OrdersLimes:0x00000001044974b0>,
  @dry_type=#<Class for mocktail of OrdersLimes:0x000000010465e758>,
  @kwargs={:shipping=>:two_day},
  @method=:order!,
  @original_method=#<UnboundMethod: OrdersLimes#order!(lime_count=..., shipping: ...),
  @original_type=OrdersLimes,
  @singleton=false>,
 #<Mocktail::Call:0x00000001046512d8
  @args=[],
  @block=nil,
  @double=#<Mocktail of OrdersLimes:0x00000001044974b0>,
  @dry_type=#<Class for mocktail of OrdersLimes:0x000000010465e758>,
  @kwargs={:shipping=>:ground},
  @method=:order!,
  @original_method=#<UnboundMethod: OrdersLimes#order!(lime_count=..., shipping: ...),
  @original_type=OrdersLimes,
  @singleton=false>]

# Inspecting the most recent call's keyword arguments:
> Mocktail.calls(orders_limes, :order!).last.kwargs
=> {:shipping=>:ground}

This is, hopefully, all you'd need to figure out why an expected invocation failed a verify check unexpectedly.

Verifying a call happened a certain number of times

Just like stubs, verify has a times keyword argument. But, where stubs will limit a stubbing to the number of times specified, verify will enforce that exactly that numer of matching invocations took place.

This isn't something you'll need every day, but if you're paranoid about erroneously making multiple lime orders, then you could ensure it was just called once:

> orders_limes.order!(5, shipping: :two_day)
=> nil
> orders_limes.order!(5, shipping: :two_day)
=> nil
> verify(times: 1) { orders_limes.order!(5, shipping: :two_day) }

As you might expect, this will raise a VerificationError because the method was called twice in the specified way instead of once. The error message tries to make this clear:

Expected mocktail of `OrdersLimes#order!' to be called like: (Mocktail::VerificationError)

  order!(5, shipping: :two_day) [1 time]

But it was actually called this way 2 times.

Adding matchers to a verification

Continuing the thread above, let's say you don't know or don't care what the shipping: keyword argument was set to. For the purposes of the test, if that doesn't matter and you just want to express that only a single order for 5 limes was made, regardless of shipping method, you can use the m.any just like we did in the stubbing section above.

To make this point, let's call order! one more time with a different shipping method:

> orders_limes.order!(5, shipping: :carrier_pigeon)
=> nil

Now we can adjust our verify call by using m.any for the shipping kwarg:

verify(times: 1) { |m| orders_limes.order!(5, shipping: m.any) }

Because we'd called the method twice in the immediately previous and once more just now. So we should expect Mocktail's error to find all three matching invocations:

Expected mocktail of `OrdersLimes#order!' to be called like: (Mocktail::VerificationError)

  order!(5, shipping: any) [1 time]

But it was actually called this way 3 times.

There it is! The expectation sees shipping: any and correctly counts that it was invoked 3 times.

Ignoring extraneous arguments entirely

Let's keep pulling the thread and continue the example above.

Suppose this isn't paranoid enough for our tastes. Maybe the method supports lots of additional optional arguments. And maybe we just really really care that the method was called once no matter what. We could do this in two ways:

  1. Verify that the method was called once, regardless of argument
  2. Split the verification in two: verify the call exactly as we expect, and assert the call count is as we expect

In general, approach #2 is better: it expresses the two intentions separately, which allows both to be made precisely.

If we'd been expecting :carrier_pigeon shipping all along, we could verify it and then check Mocktail.calls to have the right number of invocations on :order!:

> verify {  orders_limes.order!(5, shipping: :carrier_pigeon) }
=> nil
> assert_equal 1, Mocktail.calls(orders_limes, :order!).size
=> πŸ’₯ asertion failed! Expected 1 but got 3

If this is what you're trying to accomplish, this approach is not only more precise in what it asserts, it expresses the test's intent more clearly to future readers.

If, however, extraneous arguments are truly irrelevant from the perspective of the test, approach #1 may be preferable. To enable this, you can pass ignore_extra_args: true.

In our running example, we can omit all or some of the arguments and ignore_extra_args will match every invocation, ignoring the value of their other arguments. This way, we could specify that we wanted exactly one invocation of order! via carrier pigeon, no matter how many limes were ordered:

> verify(times: 1, ignore_extra_args: true) {  orders_limes.order!(shipping: :carrier_pigeon) }
=> nil

For more options and complications, check out the full documentation of the verify API.

Pulling it all together

At this point, we've covered either Mocktail's sorbet setup or untyped install. You've learned how to instantiate mocks by dependency injection, dependency inception, or class/module method replacement. And now you've been through the basics of stubbing and verifying interactions with mocked methods. You've also probably referenced the full API documentation and visited the glossary of terms a few times.

All that's left is to put it all together and write a test!

When you're ready, let's walk through a complete example test, guided by Mocktail.