README.md in mocktail-0.0.1 vs README.md in mocktail-0.0.2

- old
+ new

@@ -4,85 +4,86 @@ # 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. +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. -## First, an aperitif +## An aperitif Before getting into the details, let's demonstrate what Mocktail's API looks -like. Suppose you have a class `Negroni`: +like. Suppose you want to test a `Bartender` class: ```ruby -class Negroni - def self.ingredients - [:gin, :campari, :sweet_vermouth] +class Bartender + def initialize + @shaker = Shaker.new + @glass = Glass.new + @bar = Bar.new end - def shake!(shaker) - shaker.mix(self.class.ingredients) + 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 - - 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 +You could write an isolated unit test with Mocktail like this: -Mocktail can do a whole lot more than this, and was also designed with -descriptive error messages and common edge cases in mind: +```ruby +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 { "🎉" } -* Entire classes and modules can be replaced with `Mocktail.replace(type)` while +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 order? + +Besides a lack of hangover, Mocktail offers several advantages over other +mocking libraries: + +* **Fewer hoops to jump through**: [`Mocktail.of_next(type)`] avoids the need + for dependency injection by returning a Mocktail of the type the next time + `Type.new` is called. You can inject a fake into production code in one + line. +* **Fewer false test passes**: Arity of arguments and keyword arguments of faked + methods is enforced—no more tests that keep passing after an API changes +* **Super-duper detailed error messages when verifications fail** +* **Fake class methods**: Singleton methods on classes and modules can be + replaced with [`Mocktail.replace(type)`](#mocktailreplace) while still 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 +* **Less test setup**: Dynamic stubbings based on the arguments passed to the actual call +* **Expressive**: Built-in [argument matchers](#mocktailmatchers) and a simple + API for adding [custom matchers](#custom-matchers) +* **Powerful**: [Argument captors](#mocktailcaptor) for assertions of very + complex arguments -## Getting started +## Ready to order? -### Install +### Install the gem The main ingredient to add to your Gemfile: ```ruby gem "mocktail", group: :test ``` -### Add the DSL +### 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`.) @@ -100,15 +101,14 @@ RSpec.configure do |config| config.include Mocktail::DSL end ``` -### Clean up after each test +### Clean up when you're done -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: +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 @@ -129,12 +129,12 @@ 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 +The entire public API is listed in the [top-level module's +source](lib/mocktail.rb). 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 @@ -282,11 +282,11 @@ ``` `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: -``` +```ruby 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 @@ -488,11 +488,11 @@ ``` 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_. +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 @@ -511,10 +511,28 @@ 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: + +```ruby +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: + +```ruby +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 @@ -552,6 +570,5 @@ 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. -