mocktail 0.0.6 → 1.1.1
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 +1 -1
- data/CHANGELOG.md +22 -0
- data/Gemfile.lock +17 -17
- data/README.md +206 -74
- data/lib/mocktail/explains_nils.rb +35 -0
- data/lib/mocktail/explains_thing.rb +37 -46
- data/lib/mocktail/handles_dry_call/fulfills_stubbing/describes_unsatisfied_stubbing.rb +11 -4
- data/lib/mocktail/handles_dry_call/fulfills_stubbing.rb +13 -1
- data/lib/mocktail/matchers/any.rb +3 -1
- data/lib/mocktail/matchers/captor.rb +4 -0
- data/lib/mocktail/matchers/numeric.rb +3 -1
- data/lib/mocktail/matchers/that.rb +1 -1
- data/lib/mocktail/raises_neato_no_method_error.rb +2 -2
- data/lib/mocktail/replaces_type/redefines_singleton_methods.rb +23 -0
- data/lib/mocktail/{simulates_argument_error → share}/cleans_backtrace.rb +0 -0
- data/lib/mocktail/share/stringifies_call.rb +14 -0
- data/lib/mocktail/simulates_argument_error.rb +1 -1
- data/lib/mocktail/value/cabinet.rb +7 -1
- data/lib/mocktail/value/explanation.rb +4 -1
- data/lib/mocktail/value/fake_method_data.rb +9 -0
- data/lib/mocktail/value/{unsatisfied_stubbing.rb → unsatisfying_call.rb} +2 -1
- data/lib/mocktail/value.rb +2 -2
- data/lib/mocktail/version.rb +1 -1
- data/lib/mocktail.rb +5 -0
- metadata +7 -6
- data/lib/mocktail/value/stub_returned_nil.rb +0 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3f1b0c2f99097d0f23cb67928b5455ce5acf9fad24baee1dff64c748b60a8da9
|
4
|
+
data.tar.gz: 9595e5e672d13711a45447b8242977189700e6d0a044f9553e75d642e455f898
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5469cfd8d09da0ba752c60529cfc016acecea8b69107d07a467e576812e1d1abf74d05f3431228c4789bf3397059e249a3307dd788a7c4bce410a85f8d86d0ee
|
7
|
+
data.tar.gz: c7dea9981f4ed8f76b4f5595c0dd88d747f30371d6c5c6245f8bd498864850700e5ca9f1c1af06b67879b24bcdf90e14d5cfd7b2111838580f3fbd4554b3cbec
|
data/.github/workflows/main.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,25 @@
|
|
1
|
+
# 1.1.1
|
2
|
+
|
3
|
+
* Improve output for undefined singleton methods
|
4
|
+
([#11](https://github.com/testdouble/mocktail/pull/11) by
|
5
|
+
[@calebhearth](https://github.com/calebhearth))
|
6
|
+
|
7
|
+
# 1.1.0
|
8
|
+
|
9
|
+
* Feature: add support for passing methods to `Mocktail.explain()`
|
10
|
+
* Fix 3.1 support by bypassing highlight_error for custom NoMethodError objects
|
11
|
+
raised by Mocktail [error_highlight#20](https://github.com/ruby/error_highlight/issues/20)
|
12
|
+
|
13
|
+
# 1.0.0
|
14
|
+
|
15
|
+
* First breaking change! 🎉
|
16
|
+
* Remove support for `Mocktail.explain(nil)` because fake nil values cannot be
|
17
|
+
made falsey. Pretty big mistake
|
18
|
+
* Add `Mocktail.explain_nils` which will return explanation objects of every
|
19
|
+
call that didn't satisfy a stubbing since the last reset, including the call
|
20
|
+
site where it happened and the backtrace to try to tease out which one you're
|
21
|
+
looking for
|
22
|
+
|
1
23
|
# 0.0.6
|
2
24
|
|
3
25
|
* Require pathname, which I missed because `bundle exec` loads it. Wups!
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
mocktail (
|
4
|
+
mocktail (1.1.1)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
@@ -10,29 +10,29 @@ GEM
|
|
10
10
|
coderay (1.1.3)
|
11
11
|
docile (1.4.0)
|
12
12
|
method_source (1.0.0)
|
13
|
-
minitest (5.
|
14
|
-
parallel (1.
|
15
|
-
parser (3.
|
13
|
+
minitest (5.15.0)
|
14
|
+
parallel (1.22.1)
|
15
|
+
parser (3.1.2.0)
|
16
16
|
ast (~> 2.4.1)
|
17
17
|
pry (0.14.1)
|
18
18
|
coderay (~> 1.1)
|
19
19
|
method_source (~> 1.0)
|
20
|
-
rainbow (3.
|
20
|
+
rainbow (3.1.1)
|
21
21
|
rake (13.0.6)
|
22
|
-
regexp_parser (2.
|
22
|
+
regexp_parser (2.3.0)
|
23
23
|
rexml (3.2.5)
|
24
|
-
rubocop (1.
|
24
|
+
rubocop (1.27.0)
|
25
25
|
parallel (~> 1.10)
|
26
|
-
parser (>= 3.
|
26
|
+
parser (>= 3.1.0.0)
|
27
27
|
rainbow (>= 2.2.2, < 4.0)
|
28
28
|
regexp_parser (>= 1.8, < 3.0)
|
29
29
|
rexml
|
30
|
-
rubocop-ast (>= 1.
|
30
|
+
rubocop-ast (>= 1.16.0, < 2.0)
|
31
31
|
ruby-progressbar (~> 1.7)
|
32
32
|
unicode-display_width (>= 1.4.0, < 3.0)
|
33
|
-
rubocop-ast (1.
|
34
|
-
parser (>= 3.
|
35
|
-
rubocop-performance (1.
|
33
|
+
rubocop-ast (1.17.0)
|
34
|
+
parser (>= 3.1.1.0)
|
35
|
+
rubocop-performance (1.13.3)
|
36
36
|
rubocop (>= 1.7.0, < 2.0)
|
37
37
|
rubocop-ast (>= 0.4.0)
|
38
38
|
ruby-progressbar (1.11.0)
|
@@ -41,10 +41,10 @@ GEM
|
|
41
41
|
simplecov-html (~> 0.11)
|
42
42
|
simplecov_json_formatter (~> 0.1)
|
43
43
|
simplecov-html (0.12.3)
|
44
|
-
simplecov_json_formatter (0.1.
|
45
|
-
standard (1.
|
46
|
-
rubocop (= 1.
|
47
|
-
rubocop-performance (= 1.
|
44
|
+
simplecov_json_formatter (0.1.4)
|
45
|
+
standard (1.10.0)
|
46
|
+
rubocop (= 1.27.0)
|
47
|
+
rubocop-performance (= 1.13.3)
|
48
48
|
unicode-display_width (2.1.0)
|
49
49
|
|
50
50
|
PLATFORMS
|
@@ -60,4 +60,4 @@ DEPENDENCIES
|
|
60
60
|
standard
|
61
61
|
|
62
62
|
BUNDLED WITH
|
63
|
-
2.
|
63
|
+
2.3.6
|
data/README.md
CHANGED
@@ -553,92 +553,42 @@ them!)]
|
|
553
553
|
### Mocktail.explain
|
554
554
|
|
555
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
|
557
|
-
messages throughout the
|
556
|
+
which way, so Mocktail tries to make it a little easier on you. In addition to
|
557
|
+
returning useful messages throughout the API, the gem also includes an
|
558
|
+
introspection method `Mocktail.explain(thing)`, which returns a human-readable
|
559
|
+
`message` and a `reference` object with useful attributes (that vary depending
|
560
|
+
on the type of fake `thing` you pass in. Below are some things `explain()` can
|
561
|
+
do.
|
558
562
|
|
559
|
-
####
|
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.
|
563
|
+
#### Fake instances created by Mocktail
|
584
564
|
|
585
|
-
|
565
|
+
Any instances created by `Mocktail.of` or `Mocktail.of_next` can be passed to
|
566
|
+
`Mocktail.explain`, and they will list out all the calls and stubbings made for
|
567
|
+
each of their fake methods.
|
586
568
|
|
587
|
-
Suppose
|
588
|
-
stubbing:
|
569
|
+
Suppose these interactions have occurred:
|
589
570
|
|
590
571
|
```ruby
|
591
572
|
ice_tray = Mocktail.of(IceTray)
|
592
573
|
|
593
|
-
stubs { ice_tray.fill(:tap_water, 30) }.with { :
|
594
|
-
```
|
595
|
-
|
596
|
-
But then you find that your subject under test is just getting `nil` back and
|
597
|
-
you don't understand why:
|
574
|
+
Mocktail.stubs { ice_tray.fill(:tap_water, 30) }.with { :some_ice }
|
598
575
|
|
599
|
-
|
600
|
-
def prep
|
601
|
-
ice = ice_tray.fill(:tap_water, 50) # => nil
|
602
|
-
glass.add(ice)
|
603
|
-
end
|
576
|
+
ice_tray.fill(:tap_water, 50)
|
604
577
|
```
|
605
578
|
|
606
|
-
You can
|
607
|
-
`
|
608
|
-
as well a summary message:
|
579
|
+
You can interrogate what's going on with the fake instance by passing it to
|
580
|
+
`explain`:
|
609
581
|
|
610
582
|
```ruby
|
611
|
-
|
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:
|
583
|
+
explanation = Mocktail.explain(ice_tray)
|
626
584
|
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
fill(:tap_water, 30)
|
585
|
+
explanation.reference.type #=> IceTray
|
586
|
+
explanation.reference.double #=> The ice_tray instance
|
587
|
+
explanation.reference.calls #=> details on each invocation of each method
|
588
|
+
explanation.reference.stubbings #=> all stubbings configured for each method
|
632
589
|
```
|
633
590
|
|
634
|
-
|
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:
|
591
|
+
Calling `explanation.message` will return:
|
642
592
|
|
643
593
|
```
|
644
594
|
This is a fake `IceTray' instance.
|
@@ -653,6 +603,7 @@ It has these mocked methods:
|
|
653
603
|
`IceTray#fill' calls:
|
654
604
|
|
655
605
|
fill(:tap_water, 50)
|
606
|
+
|
656
607
|
```
|
657
608
|
|
658
609
|
#### Modules and classes with singleton methods replaced
|
@@ -662,6 +613,8 @@ passed to `Mocktail.explain()` for a summary of all the stubbing configurations
|
|
662
613
|
and calls made against its faked singleton methods for the currently running
|
663
614
|
thread.
|
664
615
|
|
616
|
+
Imagine a `Shop` class with `self.open!` and `self.close!` singleton methods:
|
617
|
+
|
665
618
|
```ruby
|
666
619
|
Mocktail.replace(Shop)
|
667
620
|
|
@@ -671,10 +624,15 @@ Shop.open!(42)
|
|
671
624
|
|
672
625
|
Shop.close!(42)
|
673
626
|
|
674
|
-
|
627
|
+
explanation = Mocktail.explain(Shop)
|
628
|
+
|
629
|
+
explanation.reference.type #=> Shop
|
630
|
+
explanation.reference.replaced_method_names #=> [:close!, :open!]
|
631
|
+
explanation.reference.calls #=> details on each invocation of each method
|
632
|
+
explanation.reference.stubbings #=> all stubbings configured for each method
|
675
633
|
```
|
676
634
|
|
677
|
-
|
635
|
+
And `explanation.message` will return:
|
678
636
|
|
679
637
|
```ruby
|
680
638
|
`Shop' is a class that has had its singleton methods faked.
|
@@ -689,15 +647,161 @@ It has these mocked methods:
|
|
689
647
|
|
690
648
|
close!(42)
|
691
649
|
|
650
|
+
close!(42)
|
651
|
+
|
692
652
|
`Shop.open!' stubbings:
|
693
653
|
|
694
654
|
open!(numeric)
|
695
655
|
|
656
|
+
open!(numeric)
|
657
|
+
|
696
658
|
`Shop.open!' calls:
|
697
659
|
|
698
660
|
open!(42)
|
661
|
+
|
662
|
+
open!(42)
|
663
|
+
```
|
664
|
+
|
665
|
+
#### Methods on faked instances and replaced types
|
666
|
+
|
667
|
+
In addition to passing the test double, you can also pass a reference to any
|
668
|
+
fake method created by Mocktail to `Mocktail.explain`:
|
669
|
+
|
670
|
+
```ruby
|
671
|
+
ice_tray = Mocktail.of(IceTray)
|
672
|
+
|
673
|
+
ice_tray.fill(:chilled, 50)
|
674
|
+
|
675
|
+
explanation = Mocktail.explain(ice_tray.method(:fill))
|
676
|
+
|
677
|
+
explanation.reference.receiver #=> a reference to the `ice_tray` instance
|
678
|
+
explanation.reference.calls #=> details on each invocation of the method
|
679
|
+
explanation.reference.stubbings #=> all stubbings configured for the method
|
680
|
+
```
|
681
|
+
|
682
|
+
The above may be handy in cases where you want to assert the number of calls of
|
683
|
+
a method outside the `Mocktail.verify` API:
|
684
|
+
|
685
|
+
```ruby
|
686
|
+
assert_equal 1, explanation.reference.calls.size
|
687
|
+
```
|
688
|
+
|
689
|
+
The explanation will also contain a `message` like this:
|
690
|
+
|
691
|
+
```
|
692
|
+
`IceTray#fill' has no stubbings.
|
693
|
+
|
694
|
+
`IceTray#fill' calls:
|
695
|
+
|
696
|
+
fill(:chilled, 50)
|
697
|
+
```
|
698
|
+
|
699
|
+
Replaced singleton methods can also be passed to `explain()`, so something like
|
700
|
+
`Mocktail.explain(Shop.method(:open!))` from the earlier example would also work
|
701
|
+
(with `Shop` being the `receiver` on the explanation's `reference`).
|
702
|
+
|
703
|
+
#### Undefined methods
|
704
|
+
|
705
|
+
There's no API for this one, but Mocktail also offers explanations for methods
|
706
|
+
that don't exist yet. You'll see this error message whenever you try to call a
|
707
|
+
method that doesn't exist on a test double. The message is designed to
|
708
|
+
facilitate "paint-by-numbers" TDD, by including a sample definition of the
|
709
|
+
method you had attempted to call that can be copy-pasted into a source listing:
|
710
|
+
|
711
|
+
```ruby
|
712
|
+
class IceTray
|
713
|
+
end
|
714
|
+
|
715
|
+
ice_tray = Mocktail.of(IceTray)
|
716
|
+
|
717
|
+
ice_tray.fill(:water_type, 30)
|
718
|
+
# => No method `IceTray#fill' exists for call: (NoMethodError)
|
719
|
+
#
|
720
|
+
# fill(:water_type, 30)
|
721
|
+
#
|
722
|
+
# Need to define the method? Here's a sample definition:
|
723
|
+
#
|
724
|
+
# def fill(water_type, arg)
|
725
|
+
# end
|
726
|
+
```
|
727
|
+
|
728
|
+
From there, you can just copy-paste the provided method stub as a starting point
|
729
|
+
for your new method:
|
730
|
+
|
731
|
+
```ruby
|
732
|
+
class IceTray
|
733
|
+
def fill(water_type, amount)
|
734
|
+
end
|
735
|
+
end
|
736
|
+
```
|
737
|
+
|
738
|
+
### Mocktail.explain_nils
|
739
|
+
|
740
|
+
Is a faked method returning `nil` and you don't understand why?
|
741
|
+
|
742
|
+
By default, methods faked by Mocktail will return `nil` when no stubbing is
|
743
|
+
satisfied. A frequent frustration, therefore, is when the way `stubs {}.with {}`
|
744
|
+
is configured does not satisfy a call the way you expected. To try to make
|
745
|
+
debugging this a little bit easier, the gem provides a top-level
|
746
|
+
`Mocktail.explain_nils` method that will return an array of summaries of every
|
747
|
+
call to a faked method that failed to satisfy any stubbings.
|
748
|
+
|
749
|
+
For example, suppose you stub this `fill` method like so:
|
750
|
+
|
751
|
+
```ruby
|
752
|
+
ice_tray = Mocktail.of(IceTray)
|
753
|
+
|
754
|
+
stubs { ice_tray.fill(:tap_water, 30) }.with { :normal_ice }
|
755
|
+
```
|
756
|
+
|
757
|
+
But then you find that your subject under test is just getting `nil` back and
|
758
|
+
you don't understand why:
|
759
|
+
|
760
|
+
```ruby
|
761
|
+
def prep
|
762
|
+
ice = ice_tray.fill(:tap_water, 50)
|
763
|
+
glass.add(ice) # => why is `ice` nil?!
|
764
|
+
end
|
699
765
|
```
|
700
766
|
|
767
|
+
Whenever you're confused by a nil, you can call `Mocktail.explain_nils` for an
|
768
|
+
array containing `UnsatisfyingCallExplanation` objects (one for each call to
|
769
|
+
a faked method that did not satisfy any configured stubbings).
|
770
|
+
|
771
|
+
The returned explanation objects will include both a `reference` object to
|
772
|
+
explore as well a summary `message`:
|
773
|
+
|
774
|
+
```ruby
|
775
|
+
def prep
|
776
|
+
ice = ice_tray.fill(:tap_water, 50)
|
777
|
+
puts Mocktail.explain_nils.first.message
|
778
|
+
glass.add(ice)
|
779
|
+
end
|
780
|
+
```
|
781
|
+
|
782
|
+
Which will print:
|
783
|
+
|
784
|
+
```
|
785
|
+
This `nil' was returned by a mocked `IceTray#fill' method
|
786
|
+
because none of its configured stubbings were satisfied.
|
787
|
+
|
788
|
+
The actual call:
|
789
|
+
|
790
|
+
fill(:tap_water, 50)
|
791
|
+
|
792
|
+
The call site:
|
793
|
+
|
794
|
+
/path/to/your/code.rb:42:in `prep'
|
795
|
+
|
796
|
+
Stubbings configured prior to this call but not satisfied by it:
|
797
|
+
|
798
|
+
fill(:tap_water, 30)
|
799
|
+
```
|
800
|
+
|
801
|
+
The `reference` object will have details of the `call` itself, an array of
|
802
|
+
`other_stubbings` defined on the faked method, and a `backtrace` to determine
|
803
|
+
which call site produced the unexpected `nil` value.
|
804
|
+
|
701
805
|
### Mocktail.reset
|
702
806
|
|
703
807
|
This one's simple: you probably want to call `Mocktail.reset` after each test,
|
@@ -705,6 +809,35 @@ but you _definitely_ want to call it if you're using `Mocktail.replace` or
|
|
705
809
|
`Mocktail.of_next` anywhere, since those will affect state that is shared across
|
706
810
|
tests.
|
707
811
|
|
812
|
+
Calling reset in a `teardown` or `after(:each)` hook will also improve the
|
813
|
+
usefulness of messages returned by `Mocktail.explain` and
|
814
|
+
`Mocktail.explain_nils`.
|
815
|
+
|
816
|
+
## References
|
817
|
+
|
818
|
+
Mocktail is designed following a somewhat academic understanding of what mocking
|
819
|
+
is and how it should be used. Below are several references on this topic.
|
820
|
+
|
821
|
+
Blog Posts and Papers:
|
822
|
+
|
823
|
+
- [Endo-Testing: Unit Testing with Mock
|
824
|
+
Objects](<https://www2.ccs.neu.edu/research/demeter/related-work/extreme-programming/MockObjectsFinal.PDF>
|
825
|
+
by Tim Mackinnon, Steve Freeman, and Philip Craig, the paper that introduced
|
826
|
+
mocking presented by the creators of mocking.
|
827
|
+
- Michael Feathers' [The Flawed Theory Behind Unit
|
828
|
+
Testing](<https://michaelfeathers.typepad.com/michael_feathers_blog/2008/06/the-flawed-theo.html>)
|
829
|
+
|
830
|
+
Books:
|
831
|
+
|
832
|
+
- [_Growing Object-Oriented Software, Guided by
|
833
|
+
Tests_](<https://bookshop.org/books/growing-object-oriented-software-guided-by-tests/9780321503626>)
|
834
|
+
by Steve Freeman and Nat Price
|
835
|
+
|
836
|
+
Talks:
|
837
|
+
|
838
|
+
- [Please don’t mock me](https://www.youtube.com/watch?v=Af4M8GMoxi4) by Justin
|
839
|
+
Searls
|
840
|
+
|
708
841
|
## Acknowledgements
|
709
842
|
|
710
843
|
Mocktail is created & maintained by the software agency [Test
|
@@ -731,4 +864,3 @@ including (but not limited to) one-on-one communications, public posts/comments,
|
|
731
864
|
code reviews, pull requests, and GitHub issues. If violations occur, Test Double
|
732
865
|
will take any action they deem appropriate for the infraction, up to and
|
733
866
|
including blocking a user from the organization's repositories.
|
734
|
-
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require_relative "share/stringifies_method_name"
|
2
|
+
require_relative "share/stringifies_call"
|
3
|
+
|
4
|
+
module Mocktail
|
5
|
+
class ExplainsNils
|
6
|
+
def initialize
|
7
|
+
@stringifies_method_name = StringifiesMethodName.new
|
8
|
+
@stringifies_call = StringifiesCall.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def explain
|
12
|
+
Mocktail.cabinet.unsatisfying_calls.map { |unsatisfying_call|
|
13
|
+
dry_call = unsatisfying_call.call
|
14
|
+
other_stubbings = unsatisfying_call.other_stubbings
|
15
|
+
|
16
|
+
UnsatisfyingCallExplanation.new(unsatisfying_call, <<~MSG)
|
17
|
+
`nil' was returned by a mocked `#{@stringifies_method_name.stringify(dry_call)}' method
|
18
|
+
because none of its configured stubbings were satisfied.
|
19
|
+
|
20
|
+
The actual call:
|
21
|
+
|
22
|
+
#{@stringifies_call.stringify(dry_call, always_parens: true)}
|
23
|
+
|
24
|
+
The call site:
|
25
|
+
|
26
|
+
#{unsatisfying_call.backtrace.first}
|
27
|
+
|
28
|
+
#{@stringifies_call.stringify_multiple(other_stubbings.map(&:recording),
|
29
|
+
nonzero_message: "Stubbings configured prior to this call but not satisfied by it",
|
30
|
+
zero_message: "No stubbings were configured on this method")}
|
31
|
+
MSG
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -9,12 +9,12 @@ module Mocktail
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def explain(thing)
|
12
|
-
if
|
13
|
-
unsatisfied_stub_explanation(thing)
|
14
|
-
elsif (double = Mocktail.cabinet.double_for_instance(thing))
|
12
|
+
if (double = Mocktail.cabinet.double_for_instance(thing))
|
15
13
|
double_explanation(double)
|
16
14
|
elsif (type_replacement = TopShelf.instance.type_replacement_if_exists_for(thing))
|
17
15
|
replaced_type_explanation(type_replacement)
|
16
|
+
elsif (fake_method_explanation = fake_method_explanation_for(thing))
|
17
|
+
fake_method_explanation
|
18
18
|
else
|
19
19
|
no_explanation(thing)
|
20
20
|
end
|
@@ -22,38 +22,37 @@ module Mocktail
|
|
22
22
|
|
23
23
|
private
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
thing
|
28
|
-
|
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.
|
25
|
+
def fake_method_explanation_for(thing)
|
26
|
+
return unless thing.is_a?(Method)
|
27
|
+
method = thing
|
28
|
+
receiver = thing.receiver
|
39
29
|
|
40
|
-
|
41
|
-
|
42
|
-
|
30
|
+
receiver_data = if (double = Mocktail.cabinet.double_for_instance(receiver))
|
31
|
+
data_for_double(double)
|
32
|
+
elsif (type_replacement = TopShelf.instance.type_replacement_if_exists_for(receiver))
|
33
|
+
data_for_type_replacement(type_replacement)
|
34
|
+
end
|
43
35
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
36
|
+
if receiver_data
|
37
|
+
FakeMethodExplanation.new(FakeMethodData.new(
|
38
|
+
receiver: receiver,
|
39
|
+
calls: receiver_data.calls,
|
40
|
+
stubbings: receiver_data.stubbings
|
41
|
+
), describe_dry_method(receiver_data, method.name))
|
42
|
+
end
|
48
43
|
end
|
49
44
|
|
50
|
-
def
|
51
|
-
|
45
|
+
def data_for_double(double)
|
46
|
+
DoubleData.new(
|
52
47
|
type: double.original_type,
|
53
48
|
double: double.dry_instance,
|
54
49
|
calls: Mocktail.cabinet.calls_for_double(double),
|
55
50
|
stubbings: Mocktail.cabinet.stubbings_for_double(double)
|
56
51
|
)
|
52
|
+
end
|
53
|
+
|
54
|
+
def double_explanation(double)
|
55
|
+
double_data = data_for_double(double)
|
57
56
|
|
58
57
|
DoubleExplanation.new(double_data, <<~MSG)
|
59
58
|
This is a fake `#{double.original_type.name}' instance.
|
@@ -65,8 +64,8 @@ module Mocktail
|
|
65
64
|
MSG
|
66
65
|
end
|
67
66
|
|
68
|
-
def
|
69
|
-
|
67
|
+
def data_for_type_replacement(type_replacement)
|
68
|
+
TypeReplacementData.new(
|
70
69
|
type: type_replacement.type,
|
71
70
|
replaced_method_names: type_replacement.replacement_methods.map(&:name).sort,
|
72
71
|
calls: Mocktail.cabinet.calls.select { |call|
|
@@ -76,6 +75,10 @@ module Mocktail
|
|
76
75
|
stubbing.recording.double == type_replacement.type
|
77
76
|
}
|
78
77
|
)
|
78
|
+
end
|
79
|
+
|
80
|
+
def replaced_type_explanation(type_replacement)
|
81
|
+
type_replacement_data = data_for_type_replacement(type_replacement)
|
79
82
|
|
80
83
|
ReplacedTypeExplanation.new(type_replacement_data, <<~MSG)
|
81
84
|
`#{type_replacement.type}' is a #{type_replacement.type.class.to_s.downcase} that has had its singleton methods faked.
|
@@ -95,35 +98,23 @@ module Mocktail
|
|
95
98
|
))
|
96
99
|
|
97
100
|
[
|
98
|
-
|
101
|
+
@stringifies_call.stringify_multiple(
|
99
102
|
double_data.stubbings.map(&:recording).select { |call|
|
100
103
|
call.method == method
|
101
104
|
},
|
102
|
-
"`#{method_name}' stubbings",
|
103
|
-
"`#{method_name}' has no stubbings"
|
105
|
+
nonzero_message: "`#{method_name}' stubbings",
|
106
|
+
zero_message: "`#{method_name}' has no stubbings"
|
104
107
|
),
|
105
|
-
|
108
|
+
@stringifies_call.stringify_multiple(
|
106
109
|
double_data.calls.select { |call|
|
107
110
|
call.method == method
|
108
111
|
},
|
109
|
-
"`#{method_name}' calls",
|
110
|
-
"`#{method_name}' has no calls"
|
112
|
+
nonzero_message: "`#{method_name}' calls",
|
113
|
+
zero_message: "`#{method_name}' has no calls"
|
111
114
|
)
|
112
115
|
].join("\n")
|
113
116
|
end
|
114
117
|
|
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
118
|
def no_explanation(thing)
|
128
119
|
NoExplanation.new(thing,
|
129
120
|
"Unfortunately, Mocktail doesn't know what this thing is: #{thing.inspect}")
|
@@ -1,12 +1,19 @@
|
|
1
|
+
require_relative "../../share/cleans_backtrace"
|
2
|
+
|
1
3
|
module Mocktail
|
2
4
|
class DescribesUnsatisfiedStubbing
|
5
|
+
def initialize
|
6
|
+
@cleans_backtrace = CleansBacktrace.new
|
7
|
+
end
|
8
|
+
|
3
9
|
def describe(dry_call)
|
4
|
-
|
10
|
+
UnsatisfyingCall.new(
|
5
11
|
call: dry_call,
|
6
12
|
other_stubbings: Mocktail.cabinet.stubbings.select { |stubbing|
|
7
|
-
|
8
|
-
|
9
|
-
|
13
|
+
dry_call.double == stubbing.recording.double &&
|
14
|
+
dry_call.method == stubbing.recording.method
|
15
|
+
},
|
16
|
+
backtrace: @cleans_backtrace.clean(Error.new).backtrace
|
10
17
|
)
|
11
18
|
end
|
12
19
|
end
|
@@ -13,13 +13,25 @@ module Mocktail
|
|
13
13
|
stubbing.satisfied!
|
14
14
|
stubbing.effect&.call(dry_call)
|
15
15
|
else
|
16
|
-
|
16
|
+
store_unsatisfying_call!(dry_call)
|
17
|
+
nil
|
17
18
|
end
|
18
19
|
end
|
19
20
|
|
20
21
|
def satisfaction(dry_call)
|
21
22
|
return if Mocktail.cabinet.demonstration_in_progress?
|
23
|
+
|
22
24
|
@finds_satisfaction.find(dry_call)
|
23
25
|
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def store_unsatisfying_call!(dry_call)
|
30
|
+
return if Mocktail.cabinet.demonstration_in_progress?
|
31
|
+
|
32
|
+
Mocktail.cabinet.store_unsatisfying_call(
|
33
|
+
@describes_unsatisfied_stubbing.describe(dry_call)
|
34
|
+
)
|
35
|
+
end
|
24
36
|
end
|
25
37
|
end
|
@@ -4,7 +4,9 @@ module Mocktail::Matchers
|
|
4
4
|
:any
|
5
5
|
end
|
6
6
|
|
7
|
-
|
7
|
+
# Change this comment to a descriptive one once this is merged:
|
8
|
+
# https://github.com/rubocop/rubocop/pull/10551
|
9
|
+
def initialize # standard:disable Style/RedundantInitialize
|
8
10
|
end
|
9
11
|
|
10
12
|
def match?(actual)
|
@@ -4,7 +4,9 @@ module Mocktail::Matchers
|
|
4
4
|
:numeric
|
5
5
|
end
|
6
6
|
|
7
|
-
|
7
|
+
# Change this comment to a descriptive one once this is merged:
|
8
|
+
# https://github.com/rubocop/rubocop/pull/10551
|
9
|
+
def initialize # standard:disable Style/RedundantInitialize
|
8
10
|
end
|
9
11
|
|
10
12
|
def match?(actual)
|
@@ -6,7 +6,7 @@ module Mocktail::Matchers
|
|
6
6
|
|
7
7
|
def initialize(&blk)
|
8
8
|
if blk.nil?
|
9
|
-
raise "The `that` matcher must be passed a block (e.g. `that { |arg| … }`)"
|
9
|
+
raise ArgumentError.new("The `that` matcher must be passed a block (e.g. `that { |arg| … }`)")
|
10
10
|
end
|
11
11
|
@blk = blk
|
12
12
|
end
|
@@ -11,14 +11,14 @@ module Mocktail
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def call(call)
|
14
|
-
raise NoMethodError
|
14
|
+
raise NoMethodError, <<~MSG, caller[1..]
|
15
15
|
No method `#{@stringifies_method_name.stringify(call)}' exists for call:
|
16
16
|
|
17
17
|
#{@stringifies_call.stringify(call, anonymous_blocks: true, always_parens: true)}
|
18
18
|
|
19
19
|
Need to define the method? Here's a sample definition:
|
20
20
|
|
21
|
-
def #{call.method}#{params(call)}
|
21
|
+
def #{"self." if call.singleton}#{call.method}#{params(call)}
|
22
22
|
end
|
23
23
|
#{corrections(call)}
|
24
24
|
MSG
|
@@ -12,6 +12,7 @@ module Mocktail
|
|
12
12
|
type.method(name)
|
13
13
|
} - [type_replacement.replacement_new]
|
14
14
|
|
15
|
+
declare_singleton_method_missing_errors!(type)
|
15
16
|
handles_dry_call = @handles_dry_call
|
16
17
|
type_replacement.replacement_methods = type_replacement.original_methods.map { |original_method|
|
17
18
|
type.singleton_class.send(:undef_method, original_method.name)
|
@@ -35,5 +36,27 @@ module Mocktail
|
|
35
36
|
type.singleton_method(original_method.name)
|
36
37
|
}
|
37
38
|
end
|
39
|
+
|
40
|
+
def declare_singleton_method_missing_errors!(type)
|
41
|
+
return if type.singleton_methods.include?(:method_missing)
|
42
|
+
|
43
|
+
raises_neato_no_method_error = RaisesNeatoNoMethodError.new
|
44
|
+
type.define_singleton_method :method_missing,
|
45
|
+
->(name, *args, **kwargs, &block) {
|
46
|
+
raises_neato_no_method_error.call(
|
47
|
+
Call.new(
|
48
|
+
singleton: true,
|
49
|
+
double: self,
|
50
|
+
original_type: type,
|
51
|
+
dry_type: self.class,
|
52
|
+
method: name,
|
53
|
+
original_method: nil,
|
54
|
+
args: args,
|
55
|
+
kwargs: kwargs,
|
56
|
+
block: block
|
57
|
+
)
|
58
|
+
)
|
59
|
+
}
|
60
|
+
end
|
38
61
|
end
|
39
62
|
end
|
File without changes
|
@@ -4,6 +4,20 @@ module Mocktail
|
|
4
4
|
"#{call.method}#{args_to_s(call, parens: always_parens)}#{blockify(call.block, anonymous: anonymous_blocks)}"
|
5
5
|
end
|
6
6
|
|
7
|
+
def stringify_multiple(calls, nonzero_message:, zero_message:,
|
8
|
+
anonymous_blocks: false, always_parens: false)
|
9
|
+
|
10
|
+
if calls.empty?
|
11
|
+
"#{zero_message}.\n"
|
12
|
+
else
|
13
|
+
<<~MSG
|
14
|
+
#{nonzero_message}:
|
15
|
+
|
16
|
+
#{calls.map { |call| " " + stringify(call) }.join("\n\n")}
|
17
|
+
MSG
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
7
21
|
private
|
8
22
|
|
9
23
|
def args_to_s(call, parens: true)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require_relative "simulates_argument_error/transforms_params"
|
2
2
|
require_relative "simulates_argument_error/reconciles_args_with_params"
|
3
3
|
require_relative "simulates_argument_error/recreates_message"
|
4
|
-
require_relative "
|
4
|
+
require_relative "share/cleans_backtrace"
|
5
5
|
require_relative "share/stringifies_call"
|
6
6
|
|
7
7
|
module Mocktail
|
@@ -3,18 +3,20 @@
|
|
3
3
|
module Mocktail
|
4
4
|
class Cabinet
|
5
5
|
attr_writer :demonstration_in_progress
|
6
|
-
attr_reader :calls, :stubbings
|
6
|
+
attr_reader :calls, :stubbings, :unsatisfying_calls
|
7
7
|
|
8
8
|
def initialize
|
9
9
|
@doubles = []
|
10
10
|
@calls = []
|
11
11
|
@stubbings = []
|
12
|
+
@unsatisfying_calls = []
|
12
13
|
@demonstration_in_progress = false
|
13
14
|
end
|
14
15
|
|
15
16
|
def reset!
|
16
17
|
@calls = []
|
17
18
|
@stubbings = []
|
19
|
+
@unsatisfying_calls = []
|
18
20
|
# Could cause an exception or prevent pollution—you decide!
|
19
21
|
@demonstration_in_progress = false
|
20
22
|
# note we don't reset doubles as they don't carry any
|
@@ -34,6 +36,10 @@ module Mocktail
|
|
34
36
|
@stubbings << stubbing
|
35
37
|
end
|
36
38
|
|
39
|
+
def store_unsatisfying_call(unsatisfying_call)
|
40
|
+
@unsatisfying_calls << unsatisfying_call
|
41
|
+
end
|
42
|
+
|
37
43
|
def demonstration_in_progress?
|
38
44
|
@demonstration_in_progress
|
39
45
|
end
|
@@ -15,7 +15,7 @@ module Mocktail
|
|
15
15
|
class NoExplanation < Explanation
|
16
16
|
end
|
17
17
|
|
18
|
-
class
|
18
|
+
class UnsatisfyingCallExplanation < Explanation
|
19
19
|
end
|
20
20
|
|
21
21
|
class DoubleExplanation < Explanation
|
@@ -23,4 +23,7 @@ module Mocktail
|
|
23
23
|
|
24
24
|
class ReplacedTypeExplanation < Explanation
|
25
25
|
end
|
26
|
+
|
27
|
+
class FakeMethodExplanation < Explanation
|
28
|
+
end
|
26
29
|
end
|
data/lib/mocktail/value.rb
CHANGED
@@ -4,11 +4,11 @@ require_relative "value/demo_config"
|
|
4
4
|
require_relative "value/double"
|
5
5
|
require_relative "value/double_data"
|
6
6
|
require_relative "value/explanation"
|
7
|
+
require_relative "value/fake_method_data"
|
7
8
|
require_relative "value/matcher_registry"
|
8
9
|
require_relative "value/signature"
|
9
10
|
require_relative "value/stubbing"
|
10
|
-
require_relative "value/stub_returned_nil"
|
11
11
|
require_relative "value/top_shelf"
|
12
12
|
require_relative "value/type_replacement"
|
13
13
|
require_relative "value/type_replacement_data"
|
14
|
-
require_relative "value/
|
14
|
+
require_relative "value/unsatisfying_call"
|
data/lib/mocktail/version.rb
CHANGED
data/lib/mocktail.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require_relative "mocktail/dsl"
|
2
2
|
require_relative "mocktail/errors"
|
3
3
|
require_relative "mocktail/explains_thing"
|
4
|
+
require_relative "mocktail/explains_nils"
|
4
5
|
require_relative "mocktail/handles_dry_call"
|
5
6
|
require_relative "mocktail/handles_dry_new_call"
|
6
7
|
require_relative "mocktail/imitates_type"
|
@@ -61,6 +62,10 @@ module Mocktail
|
|
61
62
|
ExplainsThing.new.explain(thing)
|
62
63
|
end
|
63
64
|
|
65
|
+
def self.explain_nils
|
66
|
+
ExplainsNils.new.explain
|
67
|
+
end
|
68
|
+
|
64
69
|
# Stores most transactional state about calls & stubbing configurations
|
65
70
|
# Anything returned by this is undocumented and could change at any time, so
|
66
71
|
# don't commit code that relies on it!
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mocktail
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Searls
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-05-14 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -31,6 +31,7 @@ files:
|
|
31
31
|
- lib/mocktail.rb
|
32
32
|
- lib/mocktail/dsl.rb
|
33
33
|
- lib/mocktail/errors.rb
|
34
|
+
- lib/mocktail/explains_nils.rb
|
34
35
|
- lib/mocktail/explains_thing.rb
|
35
36
|
- lib/mocktail/handles_dry_call.rb
|
36
37
|
- lib/mocktail/handles_dry_call/fulfills_stubbing.rb
|
@@ -65,12 +66,12 @@ files:
|
|
65
66
|
- lib/mocktail/replaces_type/redefines_new.rb
|
66
67
|
- lib/mocktail/replaces_type/redefines_singleton_methods.rb
|
67
68
|
- lib/mocktail/resets_state.rb
|
69
|
+
- lib/mocktail/share/cleans_backtrace.rb
|
68
70
|
- lib/mocktail/share/creates_identifier.rb
|
69
71
|
- lib/mocktail/share/determines_matching_calls.rb
|
70
72
|
- lib/mocktail/share/stringifies_call.rb
|
71
73
|
- lib/mocktail/share/stringifies_method_name.rb
|
72
74
|
- lib/mocktail/simulates_argument_error.rb
|
73
|
-
- lib/mocktail/simulates_argument_error/cleans_backtrace.rb
|
74
75
|
- lib/mocktail/simulates_argument_error/reconciles_args_with_params.rb
|
75
76
|
- lib/mocktail/simulates_argument_error/recreates_message.rb
|
76
77
|
- lib/mocktail/simulates_argument_error/transforms_params.rb
|
@@ -81,14 +82,14 @@ files:
|
|
81
82
|
- lib/mocktail/value/double.rb
|
82
83
|
- lib/mocktail/value/double_data.rb
|
83
84
|
- lib/mocktail/value/explanation.rb
|
85
|
+
- lib/mocktail/value/fake_method_data.rb
|
84
86
|
- lib/mocktail/value/matcher_registry.rb
|
85
87
|
- lib/mocktail/value/signature.rb
|
86
|
-
- lib/mocktail/value/stub_returned_nil.rb
|
87
88
|
- lib/mocktail/value/stubbing.rb
|
88
89
|
- lib/mocktail/value/top_shelf.rb
|
89
90
|
- lib/mocktail/value/type_replacement.rb
|
90
91
|
- lib/mocktail/value/type_replacement_data.rb
|
91
|
-
- lib/mocktail/value/
|
92
|
+
- lib/mocktail/value/unsatisfying_call.rb
|
92
93
|
- lib/mocktail/verifies_call.rb
|
93
94
|
- lib/mocktail/verifies_call/finds_verifiable_calls.rb
|
94
95
|
- lib/mocktail/verifies_call/raises_verification_error.rb
|
@@ -116,7 +117,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
116
117
|
- !ruby/object:Gem::Version
|
117
118
|
version: '0'
|
118
119
|
requirements: []
|
119
|
-
rubygems_version: 3.
|
120
|
+
rubygems_version: 3.3.7
|
120
121
|
signing_key:
|
121
122
|
specification_version: 4
|
122
123
|
summary: Take your objects, and make them a double
|
@@ -1,26 +0,0 @@
|
|
1
|
-
module Mocktail
|
2
|
-
class StubReturnedNil < BasicObject
|
3
|
-
attr_reader :unsatisfied_stubbing
|
4
|
-
|
5
|
-
def initialize(unsatisfied_stubbing)
|
6
|
-
@unsatisfied_stubbing = unsatisfied_stubbing
|
7
|
-
end
|
8
|
-
|
9
|
-
def was_returned_by_unsatisfied_stub?
|
10
|
-
true
|
11
|
-
end
|
12
|
-
|
13
|
-
def tap
|
14
|
-
yield self
|
15
|
-
self
|
16
|
-
end
|
17
|
-
|
18
|
-
def method_missing(name, *args, **kwargs, &blk)
|
19
|
-
nil.send(name, *args, **kwargs, &blk)
|
20
|
-
end
|
21
|
-
|
22
|
-
def respond_to_missing?(name, include_all = false)
|
23
|
-
nil.respond_to?(name, include_all)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|