mocktail 0.0.1 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +9 -3
- data/CHANGELOG.md +38 -0
- data/Gemfile.lock +2 -1
- data/README.md +242 -65
- data/bin/console +47 -1
- data/lib/mocktail/explains_thing.rb +132 -0
- data/lib/mocktail/handles_dry_call/fulfills_stubbing/describes_unsatisfied_stubbing.rb +13 -0
- data/lib/mocktail/handles_dry_call/fulfills_stubbing.rb +4 -0
- data/lib/mocktail/handles_dry_call/validates_arguments.rb +2 -23
- data/lib/mocktail/imitates_type/makes_double/declares_dry_class.rb +29 -23
- data/lib/mocktail/imitates_type/makes_double/gathers_fakeable_instance_methods.rb +21 -0
- data/lib/mocktail/imitates_type/makes_double.rb +8 -4
- data/lib/mocktail/raises_neato_no_method_error.rb +81 -0
- data/lib/mocktail/share/creates_identifier.rb +28 -0
- data/lib/mocktail/{verifies_call/raises_verification_error → share}/stringifies_call.rb +16 -7
- data/lib/mocktail/share/stringifies_method_name.rb +11 -0
- data/lib/mocktail/simulates_argument_error/cleans_backtrace.rb +15 -0
- data/lib/mocktail/simulates_argument_error/reconciles_args_with_params.rb +20 -0
- data/lib/mocktail/simulates_argument_error/recreates_message.rb +29 -0
- data/lib/mocktail/simulates_argument_error/transforms_params.rb +32 -0
- data/lib/mocktail/simulates_argument_error.rb +30 -0
- data/lib/mocktail/value/cabinet.rb +12 -0
- data/lib/mocktail/value/double.rb +7 -8
- data/lib/mocktail/value/double_data.rb +10 -0
- data/lib/mocktail/value/explanation.rb +26 -0
- data/lib/mocktail/value/signature.rb +36 -0
- data/lib/mocktail/value/stub_returned_nil.rb +26 -0
- data/lib/mocktail/value/top_shelf.rb +24 -25
- data/lib/mocktail/value/type_replacement_data.rb +13 -0
- data/lib/mocktail/value/unsatisfied_stubbing.rb +8 -0
- data/lib/mocktail/value.rb +6 -0
- data/lib/mocktail/verifies_call/raises_verification_error.rb +4 -2
- data/lib/mocktail/version.rb +1 -1
- data/lib/mocktail.rb +9 -0
- data/mocktail.gemspec +2 -2
- metadata +22 -6
- data/lib/mocktail/share/simulates_argument_error.rb +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8e043c9a28687502f8933593acca4a4860a11088b51a7f31ea726e2b80b8eb92
|
4
|
+
data.tar.gz: d406fcd7610fb29f2a6c964b777f0e77c770d273c87216872d2feeb9bcda39eb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '09b7f3e7931069bfd62993b8e91d08a92f6e11b51c5b6d21698a0f1773a72786d55d78db1c649aa9a1e935dc72e81042dd2127f15a09e0296c00dbdcb1937423'
|
7
|
+
data.tar.gz: d0ffd4f421f76afab828a426c9246a31794341fc7c5a8af862ab8f726eafad1324b3e2caa9ad2dae6ed79a34066fcd5e27be53e6dd7c70544bb3d75c900d725f
|
data/.github/workflows/main.yml
CHANGED
@@ -4,15 +4,21 @@ on: [push,pull_request]
|
|
4
4
|
|
5
5
|
jobs:
|
6
6
|
build:
|
7
|
-
|
7
|
+
strategy:
|
8
|
+
matrix:
|
9
|
+
os: [ ubuntu-latest ]
|
10
|
+
ruby-version: [3.0.1]
|
11
|
+
|
12
|
+
runs-on: ${{ matrix.os }}
|
13
|
+
|
8
14
|
steps:
|
9
15
|
- uses: actions/checkout@v2
|
10
16
|
- name: Set up Ruby
|
11
17
|
uses: ruby/setup-ruby@v1
|
12
18
|
with:
|
13
|
-
ruby-version:
|
19
|
+
ruby-version: ${{ matrix.ruby-version }}
|
14
20
|
- name: Run the default task
|
15
21
|
run: |
|
16
|
-
gem install bundler
|
22
|
+
gem install bundler
|
17
23
|
bundle install
|
18
24
|
bundle exec rake
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,41 @@
|
|
1
|
+
# 0.0.5
|
2
|
+
|
3
|
+
* Fix concurrency [#6](https://github.com/testdouble/mocktail/pull/6)
|
4
|
+
|
5
|
+
# 0.0.4
|
6
|
+
|
7
|
+
* Introduce Mocktail.explain(), which will return a message & reference object
|
8
|
+
for any of:
|
9
|
+
* A class that has been passed to Mocktail.replace()
|
10
|
+
* An instance created by Mocktail.of() or of_next()
|
11
|
+
* A nil value returned by an unsatisfied stubbing invocation
|
12
|
+
* Fix some minor printing issue with the improved NoMethodError released in
|
13
|
+
0.0.3
|
14
|
+
|
15
|
+
|
16
|
+
# 0.0.3
|
17
|
+
|
18
|
+
* Implement method_missing on all mocked instance methods to print out useful
|
19
|
+
information, like the target type, the attempted call, an example method
|
20
|
+
definition that would match the call (for paint-by-numbers-like TDD), and
|
21
|
+
did_you_mean gem integration of similar method names in case it was just a
|
22
|
+
miss
|
23
|
+
* Cleans artificially-generated argument errors of gem-internal backtraces
|
24
|
+
|
25
|
+
# 0.0.2
|
26
|
+
|
27
|
+
* Drop Ruby 2.7 support. Unbeknownst to me (since I developed mocktail using
|
28
|
+
ruby 3.0), the entire approach to using `define_method` with `*args` and
|
29
|
+
`**kwargs` splats only further confuses the [arg
|
30
|
+
splitting](https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/)
|
31
|
+
behavior in Ruby 2.x. So in the event that someone calls a method with an
|
32
|
+
ambiguous hash-as-last arg (either in a mock demonstration or call), it will
|
33
|
+
be functionally impossible to either (a) validate the args against the
|
34
|
+
parameters or (b) compare two calls as being a match for one another. These
|
35
|
+
problems could be overcome (by using `eval` instead of `define_method` for
|
36
|
+
mocked methods and by expanding the call-matching logic dramatically), but
|
37
|
+
who's got the time. Upgrade to 3.0!
|
38
|
+
|
1
39
|
# 0.0.1
|
2
40
|
|
3
41
|
Initial release
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -6,73 +6,85 @@ width="90%"/>
|
|
6
6
|
|
7
7
|
Mocktail is a [test
|
8
8
|
double](https://github.com/testdouble/contributing-tests/wiki/Test-Double)
|
9
|
-
library for Ruby
|
9
|
+
library for Ruby that provides a terse and robust API for creating mocks,
|
10
|
+
getting them in the hands of the code you're testing, stub & verify behavior,
|
11
|
+
and even safely override class methods.
|
10
12
|
|
11
|
-
##
|
13
|
+
## An aperitif
|
12
14
|
|
13
15
|
Before getting into the details, let's demonstrate what Mocktail's API looks
|
14
|
-
like. Suppose you
|
16
|
+
like. Suppose you want to test a `Bartender` class:
|
15
17
|
|
16
18
|
```ruby
|
17
|
-
class
|
18
|
-
def
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
def shake!(shaker)
|
23
|
-
shaker.mix(self.class.ingredients)
|
19
|
+
class Bartender
|
20
|
+
def initialize
|
21
|
+
@shaker = Shaker.new
|
22
|
+
@glass = Glass.new
|
23
|
+
@bar = Bar.new
|
24
24
|
end
|
25
25
|
|
26
|
-
def
|
27
|
-
|
26
|
+
def make_drink(name, customer:)
|
27
|
+
if name == :negroni
|
28
|
+
drink = @shaker.combine(:gin, :campari, :sweet_vermouth)
|
29
|
+
@glass.pour!(drink)
|
30
|
+
@bar.pass(@glass, to: customer)
|
31
|
+
end
|
28
32
|
end
|
29
33
|
end
|
30
34
|
```
|
31
35
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
*
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
*
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
36
|
+
You could write an isolated unit test with Mocktail like this:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
shaker = Mocktail.of_next(Shaker)
|
40
|
+
glass = Mocktail.of_next(Glass)
|
41
|
+
bar = Mocktail.of_next(Bar)
|
42
|
+
subject = Bartender.new
|
43
|
+
stubs { shaker.combine(:gin, :campari, :sweet_vermouth) }.with { :a_drink }
|
44
|
+
stubs { bar.pass(glass, to: "Eileen") }.with { "🎉" }
|
45
|
+
|
46
|
+
result = subject.make_drink(:negroni, customer: "Eileen")
|
47
|
+
|
48
|
+
assert_equal "🎉", result
|
49
|
+
# Oh yeah, and make sure the drink got poured! Silly side effects!
|
50
|
+
verify { glass.pour!(:a_drink) }
|
51
|
+
```
|
52
|
+
|
53
|
+
## Why Mocktail?
|
54
|
+
|
55
|
+
Besides helping you avoid a hangover, Mocktail offers several advantages over
|
56
|
+
Ruby's other mocking libraries:
|
57
|
+
|
58
|
+
* **Simpler test recipes**: [Mocktail.of_next(type)](#mocktailof_next) both
|
59
|
+
creates your mock and supplies to your subject under test in a single
|
60
|
+
one-liner. No more forcing dependency injection for the sake of your tests
|
61
|
+
* **WYSIWYG API**: Want to know how to stub a call to `phone.dial(911)`? You
|
62
|
+
just demonstrate the call with `stubs { phone.dial(911) }.with { :operator }`.
|
63
|
+
Because stubbing & verifying looks just like the actual call, your tests
|
64
|
+
becomes a sounding board for your APIs as you invent them
|
65
|
+
* **Argument validation**: Ever see a test pass after a change to a mocked
|
66
|
+
method should have broken it? Not with Mocktail, you haven't
|
67
|
+
* **Mocked class methods**: Singleton methods on modules and classes can be
|
68
|
+
faked out using [`Mocktail.replace(type)`](#mocktailreplace) without
|
69
|
+
sacrificing thread safety
|
70
|
+
* **Super-duper detailed error messages** A good mocking library should make
|
71
|
+
coding feel like
|
72
|
+
[paint-by-number](https://en.wikipedia.org/wiki/Paint_by_number), thoughtfully
|
73
|
+
guiding you from one step to the next. Calling a method that doesn't exist
|
74
|
+
will print a sample definition you can copy-paste. Verification failures will
|
75
|
+
print every call that _did_ occur. And [Mocktail.explain()](#mocktailexplain)
|
76
|
+
provides even more introspection
|
77
|
+
* **Expressive**: Built-in [argument matchers](#mocktailmatchers) and a simple
|
78
|
+
API for adding [custom matchers](#custom-matchers) allow you to tune your
|
79
|
+
stubbing configuration and call verification to match _exactly_ what your test
|
80
|
+
intends
|
81
|
+
* **Powerful**: [Argument captors](#mocktailcaptor) for assertions of very
|
82
|
+
complex arguments, as well as advanced configuration options for stubbing &
|
83
|
+
verification
|
84
|
+
|
85
|
+
## Ready to order?
|
86
|
+
|
87
|
+
### Install the gem
|
76
88
|
|
77
89
|
The main ingredient to add to your Gemfile:
|
78
90
|
|
@@ -80,7 +92,7 @@ The main ingredient to add to your Gemfile:
|
|
80
92
|
gem "mocktail", group: :test
|
81
93
|
```
|
82
94
|
|
83
|
-
###
|
95
|
+
### Sprinkle in the DSL
|
84
96
|
|
85
97
|
Then, in each of your tests or in a test helper, you'll probably want to include
|
86
98
|
Mocktail's DSL. (This is optional, however, as every method in the DSL is also
|
@@ -102,11 +114,10 @@ RSpec.configure do |config|
|
|
102
114
|
end
|
103
115
|
```
|
104
116
|
|
105
|
-
### Clean up
|
117
|
+
### Clean up when you're done
|
106
118
|
|
107
|
-
|
108
|
-
|
109
|
-
also call `Mocktail.reset` after each test:
|
119
|
+
To reset Mocktail's internal state between tests and avoid test pollution, you
|
120
|
+
should also call `Mocktail.reset` after each test:
|
110
121
|
|
111
122
|
In Minitest:
|
112
123
|
|
@@ -131,8 +142,8 @@ end
|
|
131
142
|
|
132
143
|
## API
|
133
144
|
|
134
|
-
The public API is
|
135
|
-
source](lib/mocktail.rb).
|
145
|
+
The entire public API is listed in the [top-level module's
|
146
|
+
source](lib/mocktail.rb). Below is a longer menu to explain what goes into each
|
136
147
|
feature.
|
137
148
|
|
138
149
|
### Mocktail.of
|
@@ -284,7 +295,7 @@ user_repository.find(1) # => :not_found
|
|
284
295
|
`ignore_extra_args` will allow a demonstration to be considered satisfied even
|
285
296
|
if it fails to specify arguments and keyword arguments made by the actual call:
|
286
297
|
|
287
|
-
```
|
298
|
+
```ruby
|
288
299
|
stubs { user_repository.find(4) }.with { :a_person }
|
289
300
|
user_repository.find(4, debug: true) # => nil
|
290
301
|
|
@@ -490,7 +501,7 @@ verify { big_api.send(payload_captor.capture) } # => nil!
|
|
490
501
|
The `verify` above will pass because _a_ call did happen, but we haven't
|
491
502
|
asserted anything beyond that yet. What really happened is that
|
492
503
|
`payload_captor.capture` actually returned a matcher that will return true for
|
493
|
-
any argument _while also sneakily storing a copy of the argument value_.
|
504
|
+
any argument _while also sneakily storing a copy of the argument value_.
|
494
505
|
|
495
506
|
That's why we instantiated `payload_captor` with `Mocktail.captor` outside the
|
496
507
|
demonstration block, so we can inspect its `value` after the `verify` call:
|
@@ -513,6 +524,24 @@ When you call `Mocktail.replace(type)`, all of the singleton methods on the
|
|
513
524
|
provided type are replaced with fake methods available for stubbing and
|
514
525
|
verification. It's really that simple.
|
515
526
|
|
527
|
+
For example, if our `Bartender` class has a class method:
|
528
|
+
|
529
|
+
```ruby
|
530
|
+
class Bartender
|
531
|
+
def self.cliche_greeting
|
532
|
+
["It's 5 o'clock somewhere!", "Norm!"].sample
|
533
|
+
end
|
534
|
+
end
|
535
|
+
```
|
536
|
+
|
537
|
+
We can replace the behavior of the overall class, and then stub how we'd like it
|
538
|
+
to respond, in our test:
|
539
|
+
|
540
|
+
```ruby
|
541
|
+
Mocktail.replace(Bartender)
|
542
|
+
stubs { Bartender.cliche_greeting }.with { "Norm!" }
|
543
|
+
```
|
544
|
+
|
516
545
|
[**Obligatory warning:** Mocktail does its best to ensure that other threads
|
517
546
|
won't be affected when you replace the singleton methods on a type, but your
|
518
547
|
mileage may very! Singleton methods are global and code that introspects or
|
@@ -521,6 +550,154 @@ down bugs. (If this concerns you, then the fact that class methods are
|
|
521
550
|
effectively global state may be a great reason not to rely too heavily on
|
522
551
|
them!)]
|
523
552
|
|
553
|
+
### Mocktail.explain
|
554
|
+
|
555
|
+
Test debugging is hard enough when there _aren't_ fake objects flying every
|
556
|
+
which way, so Mocktail tries to make it a little easier by way of better
|
557
|
+
messages throughout the library.
|
558
|
+
|
559
|
+
#### Undefined methods
|
560
|
+
|
561
|
+
One message you'll see automatically if you try to call a method
|
562
|
+
that doesn't exist is this one, which gives a sample definition of the method
|
563
|
+
you had attempted to call:
|
564
|
+
|
565
|
+
```ruby
|
566
|
+
class IceTray
|
567
|
+
end
|
568
|
+
|
569
|
+
ice_tray = Mocktail.of(IceTray)
|
570
|
+
|
571
|
+
ice_tray.fill(:water_type, 30)
|
572
|
+
# => No method `IceTray#fill' exists for call: (NoMethodError)
|
573
|
+
#
|
574
|
+
# fill(:water_type, 30)
|
575
|
+
#
|
576
|
+
# Need to define the method? Here's a sample definition:
|
577
|
+
#
|
578
|
+
# def fill(water_type, arg)
|
579
|
+
# end
|
580
|
+
```
|
581
|
+
|
582
|
+
From there, you can just copy-paste the provided method stub as a starting point
|
583
|
+
for your new method.
|
584
|
+
|
585
|
+
#### `nil` values returned by faked methods
|
586
|
+
|
587
|
+
Suppose you go ahead and implement the `fill` method above and configure a
|
588
|
+
stubbing:
|
589
|
+
|
590
|
+
```ruby
|
591
|
+
ice_tray = Mocktail.of(IceTray)
|
592
|
+
|
593
|
+
stubs { ice_tray.fill(:tap_water, 30) }.with { :normal_ice }
|
594
|
+
```
|
595
|
+
|
596
|
+
But then you find that your subject under test is just getting `nil` back and
|
597
|
+
you don't understand why:
|
598
|
+
|
599
|
+
```ruby
|
600
|
+
def prep
|
601
|
+
ice = ice_tray.fill(:tap_water, 50) # => nil
|
602
|
+
glass.add(ice)
|
603
|
+
end
|
604
|
+
```
|
605
|
+
|
606
|
+
You can pass that `nil` value to `Mocktail.explain` and get an
|
607
|
+
`UnsatisfiedStubExplanation` that will include both a `reference` object to explore
|
608
|
+
as well a summary message:
|
609
|
+
|
610
|
+
```ruby
|
611
|
+
def prep
|
612
|
+
ice = ice_tray.fill(:tap_water, 50).tap do |wat|
|
613
|
+
puts Mocktail.explain(wat).message
|
614
|
+
end
|
615
|
+
glass.add(ice)
|
616
|
+
end
|
617
|
+
```
|
618
|
+
|
619
|
+
Which will print:
|
620
|
+
|
621
|
+
```
|
622
|
+
This `nil' was returned by a mocked `IceTray#fill' method
|
623
|
+
because none of its configured stubbings were satisfied.
|
624
|
+
|
625
|
+
The actual call:
|
626
|
+
|
627
|
+
fill(:tap_water, 50)
|
628
|
+
|
629
|
+
Stubbings configured prior to this call but not satisfied by it:
|
630
|
+
|
631
|
+
fill(:tap_water, 30)
|
632
|
+
```
|
633
|
+
|
634
|
+
#### Fake instances created by Mocktail
|
635
|
+
|
636
|
+
Any instances created by `Mocktail.of` or `Mocktail.of_next` can also be passed
|
637
|
+
to `Mocktail.explain`, and they will list out all the calls and stubbings made
|
638
|
+
for each of their fake methods.
|
639
|
+
|
640
|
+
Calling `Mocktail.explain(ice_tray).message` following the example above will
|
641
|
+
yield:
|
642
|
+
|
643
|
+
```
|
644
|
+
This is a fake `IceTray' instance.
|
645
|
+
|
646
|
+
It has these mocked methods:
|
647
|
+
- fill
|
648
|
+
|
649
|
+
`IceTray#fill' stubbings:
|
650
|
+
|
651
|
+
fill(:tap_water, 30)
|
652
|
+
|
653
|
+
`IceTray#fill' calls:
|
654
|
+
|
655
|
+
fill(:tap_water, 50)
|
656
|
+
```
|
657
|
+
|
658
|
+
#### Modules and classes with singleton methods replaced
|
659
|
+
|
660
|
+
If you've called `Mocktail.replace()` on a class or module, it can also be
|
661
|
+
passed to `Mocktail.explain()` for a summary of all the stubbing configurations
|
662
|
+
and calls made against its faked singleton methods for the currently running
|
663
|
+
thread.
|
664
|
+
|
665
|
+
```ruby
|
666
|
+
Mocktail.replace(Shop)
|
667
|
+
|
668
|
+
stubs { |m| Shop.open!(m.numeric) }.with { :a_bar }
|
669
|
+
|
670
|
+
Shop.open!(42)
|
671
|
+
|
672
|
+
Shop.close!(42)
|
673
|
+
|
674
|
+
puts Mocktail.explain(Shop).message
|
675
|
+
```
|
676
|
+
|
677
|
+
Will print:
|
678
|
+
|
679
|
+
```ruby
|
680
|
+
`Shop' is a class that has had its singleton methods faked.
|
681
|
+
|
682
|
+
It has these mocked methods:
|
683
|
+
- close!
|
684
|
+
- open!
|
685
|
+
|
686
|
+
`Shop.close!' has no stubbings.
|
687
|
+
|
688
|
+
`Shop.close!' calls:
|
689
|
+
|
690
|
+
close!(42)
|
691
|
+
|
692
|
+
`Shop.open!' stubbings:
|
693
|
+
|
694
|
+
open!(numeric)
|
695
|
+
|
696
|
+
`Shop.open!' calls:
|
697
|
+
|
698
|
+
open!(42)
|
699
|
+
```
|
700
|
+
|
524
701
|
### Mocktail.reset
|
525
702
|
|
526
703
|
This one's simple: you probably want to call `Mocktail.reset` after each test,
|
data/bin/console
CHANGED
@@ -29,7 +29,53 @@ class Auditor
|
|
29
29
|
def record!(message, user:, action: nil); end
|
30
30
|
end
|
31
31
|
|
32
|
+
class Shaker
|
33
|
+
def combine(*args); end
|
34
|
+
end
|
35
|
+
class Glass
|
36
|
+
def pour!(drink); end
|
37
|
+
end
|
38
|
+
class Bar
|
39
|
+
def pass(glass, to:)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Bartender
|
44
|
+
def initialize
|
45
|
+
@shaker = Shaker.new
|
46
|
+
@glass = Glass.new
|
47
|
+
@bar = Bar.new
|
48
|
+
end
|
49
|
+
|
50
|
+
def make_drink(name, customer:)
|
51
|
+
if name == :negroni
|
52
|
+
drink = @shaker.combine(:gin, :campari, :sweet_vermouth)
|
53
|
+
@glass.pour!(drink)
|
54
|
+
@bar.pass(@glass, to: customer)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class IceTray
|
60
|
+
def fill(water_type, amount)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class Shop
|
65
|
+
def self.open!(bar_id)
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.close!(bar_id)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
Mocktail.replace(Shop)
|
73
|
+
|
74
|
+
stubs { |m| Shop.open!(m.numeric) }.with { :a_bar }
|
75
|
+
|
76
|
+
Shop.open!(42)
|
77
|
+
|
78
|
+
Shop.close!(42)
|
32
79
|
|
33
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
34
80
|
require "pry"
|
35
81
|
Pry.start
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require_relative "share/stringifies_method_name"
|
2
|
+
require_relative "share/stringifies_call"
|
3
|
+
|
4
|
+
module Mocktail
|
5
|
+
class ExplainsThing
|
6
|
+
def initialize
|
7
|
+
@stringifies_method_name = StringifiesMethodName.new
|
8
|
+
@stringifies_call = StringifiesCall.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def explain(thing)
|
12
|
+
if is_stub_returned_nil?(thing)
|
13
|
+
unsatisfied_stub_explanation(thing)
|
14
|
+
elsif (double = Mocktail.cabinet.double_for_instance(thing))
|
15
|
+
double_explanation(double)
|
16
|
+
elsif (type_replacement = TopShelf.instance.type_replacement_if_exists_for(thing))
|
17
|
+
replaced_type_explanation(type_replacement)
|
18
|
+
else
|
19
|
+
no_explanation(thing)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# Our fake nil doesn't even implement respond_to?, instead quacking like nil
|
26
|
+
def is_stub_returned_nil?(thing)
|
27
|
+
thing.was_returned_by_unsatisfied_stub?
|
28
|
+
rescue NoMethodError
|
29
|
+
end
|
30
|
+
|
31
|
+
def unsatisfied_stub_explanation(stub_returned_nil)
|
32
|
+
unsatisfied_stubbing = stub_returned_nil.unsatisfied_stubbing
|
33
|
+
dry_call = unsatisfied_stubbing.call
|
34
|
+
other_stubbings = unsatisfied_stubbing.other_stubbings
|
35
|
+
|
36
|
+
UnsatisfiedStubExplanation.new(unsatisfied_stubbing, <<~MSG)
|
37
|
+
This `nil' was returned by a mocked `#{@stringifies_method_name.stringify(dry_call)}' method
|
38
|
+
because none of its configured stubbings were satisfied.
|
39
|
+
|
40
|
+
The actual call:
|
41
|
+
|
42
|
+
#{@stringifies_call.stringify(dry_call, always_parens: true)}
|
43
|
+
|
44
|
+
#{describe_multiple_calls(other_stubbings.map(&:recording),
|
45
|
+
"Stubbings configured prior to this call but not satisfied by it",
|
46
|
+
"No stubbings were configured on this method")}
|
47
|
+
MSG
|
48
|
+
end
|
49
|
+
|
50
|
+
def double_explanation(double)
|
51
|
+
double_data = DoubleData.new(
|
52
|
+
type: double.original_type,
|
53
|
+
double: double.dry_instance,
|
54
|
+
calls: Mocktail.cabinet.calls_for_double(double),
|
55
|
+
stubbings: Mocktail.cabinet.stubbings_for_double(double)
|
56
|
+
)
|
57
|
+
|
58
|
+
DoubleExplanation.new(double_data, <<~MSG)
|
59
|
+
This is a fake `#{double.original_type.name}' instance.
|
60
|
+
|
61
|
+
It has these mocked methods:
|
62
|
+
#{double.dry_methods.sort.map { |method| " - #{method}" }.join("\n")}
|
63
|
+
|
64
|
+
#{double.dry_methods.sort.map { |method| describe_dry_method(double_data, method) }.join("\n")}
|
65
|
+
MSG
|
66
|
+
end
|
67
|
+
|
68
|
+
def replaced_type_explanation(type_replacement)
|
69
|
+
type_replacement_data = TypeReplacementData.new(
|
70
|
+
type: type_replacement.type,
|
71
|
+
replaced_method_names: type_replacement.replacement_methods.map(&:name).sort,
|
72
|
+
calls: Mocktail.cabinet.calls.select { |call|
|
73
|
+
call.double == type_replacement.type
|
74
|
+
},
|
75
|
+
stubbings: Mocktail.cabinet.stubbings.select { |stubbing|
|
76
|
+
stubbing.recording.double == type_replacement.type
|
77
|
+
}
|
78
|
+
)
|
79
|
+
|
80
|
+
ReplacedTypeExplanation.new(type_replacement_data, <<~MSG)
|
81
|
+
`#{type_replacement.type}' is a #{type_replacement.type.class.to_s.downcase} that has had its singleton methods faked.
|
82
|
+
|
83
|
+
It has these mocked methods:
|
84
|
+
#{type_replacement_data.replaced_method_names.map { |method| " - #{method}" }.join("\n")}
|
85
|
+
|
86
|
+
#{type_replacement_data.replaced_method_names.map { |method| describe_dry_method(type_replacement_data, method) }.join("\n")}
|
87
|
+
MSG
|
88
|
+
end
|
89
|
+
|
90
|
+
def describe_dry_method(double_data, method)
|
91
|
+
method_name = @stringifies_method_name.stringify(Call.new(
|
92
|
+
original_type: double_data.type,
|
93
|
+
singleton: double_data.type == double_data.double,
|
94
|
+
method: method
|
95
|
+
))
|
96
|
+
|
97
|
+
[
|
98
|
+
describe_multiple_calls(
|
99
|
+
double_data.stubbings.map(&:recording).select { |call|
|
100
|
+
call.method == method
|
101
|
+
},
|
102
|
+
"`#{method_name}' stubbings",
|
103
|
+
"`#{method_name}' has no stubbings"
|
104
|
+
),
|
105
|
+
describe_multiple_calls(
|
106
|
+
double_data.calls.select { |call|
|
107
|
+
call.method == method
|
108
|
+
},
|
109
|
+
"`#{method_name}' calls",
|
110
|
+
"`#{method_name}' has no calls"
|
111
|
+
)
|
112
|
+
].join("\n")
|
113
|
+
end
|
114
|
+
|
115
|
+
def describe_multiple_calls(calls, nonzero_message, zero_message)
|
116
|
+
if calls.empty?
|
117
|
+
"#{zero_message}.\n"
|
118
|
+
else
|
119
|
+
<<~MSG
|
120
|
+
#{nonzero_message}:
|
121
|
+
|
122
|
+
#{calls.map { |call| " " + @stringifies_call.stringify(call) }.join("\n\n")}
|
123
|
+
MSG
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def no_explanation(thing)
|
128
|
+
NoExplanation.new(thing,
|
129
|
+
"Unfortunately, Mocktail doesn't know what this thing is: #{thing.inspect}")
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Mocktail
|
2
|
+
class DescribesUnsatisfiedStubbing
|
3
|
+
def describe(dry_call)
|
4
|
+
UnsatisfiedStubbing.new(
|
5
|
+
call: dry_call,
|
6
|
+
other_stubbings: Mocktail.cabinet.stubbings.select { |stubbing|
|
7
|
+
dry_call.double == stubbing.recording.double &&
|
8
|
+
dry_call.method == stubbing.recording.method
|
9
|
+
}
|
10
|
+
)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|