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.
-