mocktail 0.0.1 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|