# Mocktail 🍸 Mocktail is a [test double](https://github.com/testdouble/contributing-tests/wiki/Test-Double) library for Ruby. It offers a simple API and robust feature-set. ## First, an aperitif Before getting into the details, let's demonstrate what Mocktail's API looks like. Suppose you have a class `Negroni`: ```ruby class Negroni def self.ingredients [:gin, :campari, :sweet_vermouth] end def shake!(shaker) shaker.mix(self.class.ingredients) end def sip(amount) raise "unimplemented" end end ``` 1. Create a mocked instance: `negroni = Mocktail.of(Negroni)` 2. Stub a response with `stubs { negroni.sip(4) }.with { :ahh }` * Calling `negroni.sip(4)` will subsequently return `:ahh` * Another example: `stubs { |m| negroni.sip(m.numeric) }.with { :nice }` 3. Verify a call with `verify { negroni.shake!(:some_shaker) }` * `verify` will raise an error unless `negroni.shake!(:some_shaker)` has been called * Another example: `verify { |m| negroni.shake!(m.that { |arg| arg.respond_to?(:mix) }) }` 4. Deliver a mock to your code under test with `negroni = Mocktail.of_next(Negroni)` * `of_next` will return a fake `Negroni` * The next call to `Negroni.new` will return _exactly the same_ fake instance, allowing the code being tested to seamlessly instantiate and interact with it * This means no dependency injection is necessary, nor is a sweeping override like [any_instance](https://relishapp.com/rspec/rspec-mocks/docs/working-with-legacy-code/any-instance) * `Negroni.new` will be unaffected on other threads and will continue behaving like normal as soon as the next `new` call Mocktail can do a whole lot more than this, and was also designed with descriptive error messages and common edge cases in mind: * Entire classes and modules can be replaced with `Mocktail.replace(type)` while preserving thread safety * Arity of arguments and keyword arguments is enforced on faked methods to prevent isolated unit tests from continuing to pass after an API contract changes * For mocked methods that take a block, `stubs` & `verify` can inspect and invoke the passed block to determine whether the call satisfies their conditions * Dynamic stubbings that return a value based on how the mocked method was called * Advanced stubbing and verification options like specifying the number of `times` a stub can be satisfied or a call should be verified, allowing tests to forego specifying arguments and blocks, and temporarily disabling arity validation * Built-in matchers as well as custom matcher support * Argument captors for complex, multi-step call verifications ## Getting started ### Install The main ingredient to add to your Gemfile: ```ruby gem "mocktail", group: :test ``` ### Add 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: ```ruby class Minitest::Test include Mocktail::DSL end ``` Or, in RSpec: ```ruby RSpec.configure do |config| config.include Mocktail::DSL end ``` ### Clean up after each test When making so many concoctions, it's important to keep a clean bar! To reset Mocktail's internal state between tests and avoid test pollution, you should also call `Mocktail.reset` after each test: In Minitest: ```ruby class Minitest::Test # Or, if in a Rails test, in a `teardown do…end` block def teardown Mocktail.reset end end ``` And RSpec: ```ruby RSpec.configure do |config| config.after(:each) do Mocktail.reset end end ``` ## API The public API is a pretty quick read of the [top-level module's source](lib/mocktail.rb). Here's 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. ```ruby 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_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: ```ruby class Notifier def initialize @mailer = Mailer.new end def notify(name) @mailer.deliver!("Hello, #{name}") end end ``` You could write a test like this: ```ruby 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: ```ruby 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: ```ruby 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: ```ruby 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: ```ruby 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: ```ruby 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: ```ruby 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: ```ruby 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. ```ruby 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: ```ruby 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: ```ruby 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: ```ruby 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: ```ruby 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 exactly `times` 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 satisfied * `ignore_block` will similarly allow the demonstration to forego specifying a block, even if the actual call receives one ### 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: ```ruby 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 keyword * `is_a(type)` - Will match when its `type` passes an `is_a?` check against the actual argument * `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?` * `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?` * `not(thing)` - Will only match when its argument _does not_ equal (via `!=`) the actual argument * `numeric` - Will match when the actual argument is an instance of `Integer`, `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](lib/mocktail/matchers/includes.rb). Once you've implemented a class, just pass it to `Mocktail.register_matcher` in your test helper. ```ruby 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: ```ruby 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: ```ruby class BigApi def send(payload); end end ``` You could capture the value of that payload as part of the verification of the call: ```ruby 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: ```ruby 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. [**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.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. ## Acknowledgements Mocktail is created & maintained by the software agency [Test Double](https://twitter.com). If you've ever come across our eponymously-named [testdouble.js](https://github.com/testdouble/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](http://xunitpatterns.com/Test%20Double.html). The name is inspired by the innovative Java mocking library [Mockito](https://site.mockito.org). Mocktail also the spiritual successor to [gimme](https://github.com/searls/gimme), which offers a similar API but which fell victim to the limitations of Ruby 1.8.7 (and [@searls](https://twitter.com/searls)'s Ruby chops). Gimme was also one of the final projects we collaborated with [Jim Weirich](https://github.com/jimweirich) 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](https://testdouble.com/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.