README.md in mocktail-0.0.3 vs README.md in mocktail-0.0.4

- old
+ new

@@ -48,30 +48,41 @@ assert_equal "🎉", result # Oh yeah, and make sure the drink got poured! Silly side effects! verify { glass.pour!(:a_drink) } ``` -## Why order? +## Why Mocktail? -Besides a lack of hangover, Mocktail offers several advantages over other -mocking libraries: +Besides helping you avoid a hangover, Mocktail offers several advantages over +Ruby's 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 -* **Less test setup**: Dynamic stubbings based on the arguments passed to the actual call +* **Simpler test recipes**: [Mocktail.of_next(type)](#mocktailof_next) both + creates your mock and supplies to your subject under test in a single + one-liner. No more forcing dependency injection for the sake of your tests +* **WYSIWYG API**: Want to know how to stub a call to `phone.dial(911)`? You + just demonstrate the call with `stubs { phone.dial(911) }.with { :operator }`. + Because stubbing & verifying looks just like the actual call, your tests + becomes a sounding board for your APIs as you invent them +* **Argument validation**: Ever see a test pass after a change to a mocked + method should have broken it? Not with Mocktail, you haven't +* **Mocked class methods**: Singleton methods on modules and classes can be + faked out using [`Mocktail.replace(type)`](#mocktailreplace) without + sacrificing thread safety +* **Super-duper detailed error messages** A good mocking library should make + coding feel like + [paint-by-number](https://en.wikipedia.org/wiki/Paint_by_number), thoughtfully + guiding you from one step to the next. Calling a method that doesn't exist + will print a sample definition you can copy-paste. Verification failures will + print every call that _did_ occur. And [Mocktail.explain()](#mocktailexplain) + provides even more introspection * **Expressive**: Built-in [argument matchers](#mocktailmatchers) and a simple - API for adding [custom matchers](#custom-matchers) + API for adding [custom matchers](#custom-matchers) allow you to tune your + stubbing configuration and call verification to match _exactly_ what your test + intends * **Powerful**: [Argument captors](#mocktailcaptor) for assertions of very - complex arguments + complex arguments, as well as advanced configuration options for stubbing & + verification ## Ready to order? ### Install the gem @@ -536,9 +547,157 @@ 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.explain + +Test debugging is hard enough when there _aren't_ fake objects flying every +which way, so Mocktail tries to make it a little easier by way of better +messages throughout the library. + +#### Undefined methods + +One message you'll see automatically if you try to call a method +that doesn't exist is this one, which gives a sample definition of the method +you had attempted to call: + +```ruby +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. + +#### `nil` values returned by faked methods + +Suppose you go ahead and implement the `fill` method above and configure a +stubbing: + +```ruby +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: + +```ruby +def prep + ice = ice_tray.fill(:tap_water, 50) # => nil + glass.add(ice) +end +``` + +You can pass that `nil` value to `Mocktail.explain` and get an +`UnsatisfiedStubExplanation` that will include both a `reference` object to explore + as well a summary message: + +```ruby +def prep + ice = ice_tray.fill(:tap_water, 50).tap do |wat| + puts Mocktail.explain(wat).message + end + 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) + +Stubbings configured prior to this call but not satisfied by it: + + fill(:tap_water, 30) +``` + +#### Fake instances created by Mocktail + +Any instances created by `Mocktail.of` or `Mocktail.of_next` can also be passed +to `Mocktail.explain`, and they will list out all the calls and stubbings made +for each of their fake methods. + +Calling `Mocktail.explain(ice_tray).message` following the example above will +yield: + +``` +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) +``` + +#### Modules and classes with singleton methods replaced + +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 singleton methods for the currently running +thread. + +```ruby +Mocktail.replace(Shop) + +stubs { |m| Shop.open!(m.numeric) }.with { :a_bar } + +Shop.open!(42) + +Shop.close!(42) + +puts Mocktail.explain(Shop).message +``` + +Will print: + +```ruby +`Shop' is a class that has had its singleton methods faked. + +It has these mocked methods: + - close! + - open! + +`Shop.close!' has no stubbings. + +`Shop.close!' calls: + + close!(42) + +`Shop.open!' stubbings: + + open!(numeric) + +`Shop.open!' calls: + + open!(42) +``` ### 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