mocktail 0.0.4 → 1.1.0
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 +1 -1
- data/CHANGELOG.md +25 -1
- data/Gemfile.lock +14 -14
- data/README.md +182 -73
- data/lib/mocktail/explains_nils.rb +35 -0
- data/lib/mocktail/explains_thing.rb +39 -48
- 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/captor.rb +4 -0
- data/lib/mocktail/matchers/that.rb +1 -1
- data/lib/mocktail/raises_neato_no_method_error.rb +1 -1
- data/lib/mocktail/{simulates_argument_error → share}/cleans_backtrace.rb +2 -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/top_shelf.rb +21 -26
- 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
- data/mocktail.gemspec +1 -1
- metadata +8 -7
- 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: 6a9c74cd3ff5167b99c38892ee6411a1ac3f1379158c062076bfbe6d8479c7b7
|
4
|
+
data.tar.gz: 81dd2b80a78c162a9c4030fa8bc9be42741c3a8c053fa33c319dc13c404234a8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bd1fbc482135e89dea00de9249d63b5e851d64ecb2f4afc898b4bb77abf5afc96303cfed6b59665d0e51ab4b2c8dd4c438dc2a7ab93d1c69e1d12d723797cf9a
|
7
|
+
data.tar.gz: 3af137dd6d1488b1ef41ac0b47178a224e160f11dce0ed0cbeb24418ae4a33c86a6e4fa8e3cabb20c8421c8ac1ac8c166045051d996d4aa1dc709e4fcc97ec3a
|
data/.github/workflows/main.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,28 @@
|
|
1
|
-
#
|
1
|
+
# 1.1.0
|
2
|
+
|
3
|
+
* Feature: add support for passing methods to `Mocktail.explain()`
|
4
|
+
* Fix 3.1 support by bypassing highlight_error for custom NoMethodError objects
|
5
|
+
raised by Mocktail [error_highlight#20](https://github.com/ruby/error_highlight/issues/20)
|
6
|
+
|
7
|
+
# 1.0.0
|
8
|
+
|
9
|
+
* First breaking change! 🎉
|
10
|
+
* Remove support for `Mocktail.explain(nil)` because fake nil values cannot be
|
11
|
+
made falsey. Pretty big mistake
|
12
|
+
* Add `Mocktail.explain_nils` which will return explanation objects of every
|
13
|
+
call that didn't satisfy a stubbing since the last reset, including the call
|
14
|
+
site where it happened and the backtrace to try to tease out which one you're
|
15
|
+
looking for
|
16
|
+
|
17
|
+
# 0.0.6
|
18
|
+
|
19
|
+
* Require pathname, which I missed because `bundle exec` loads it. Wups!
|
20
|
+
|
21
|
+
# 0.0.5
|
22
|
+
|
23
|
+
* Fix concurrency [#6](https://github.com/testdouble/mocktail/pull/6)
|
24
|
+
|
25
|
+
# 0.0.4
|
2
26
|
|
3
27
|
* Introduce Mocktail.explain(), which will return a message & reference object
|
4
28
|
for any of:
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
mocktail (
|
4
|
+
mocktail (1.1.0)
|
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.
|
13
|
+
minitest (5.15.0)
|
14
14
|
parallel (1.21.0)
|
15
|
-
parser (3.0.
|
15
|
+
parser (3.1.0.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.2.0)
|
23
23
|
rexml (3.2.5)
|
24
|
-
rubocop (1.
|
24
|
+
rubocop (1.25.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.15.1, < 2.0)
|
31
31
|
ruby-progressbar (~> 1.7)
|
32
32
|
unicode-display_width (>= 1.4.0, < 3.0)
|
33
|
-
rubocop-ast (1.
|
33
|
+
rubocop-ast (1.15.1)
|
34
34
|
parser (>= 3.0.1.1)
|
35
|
-
rubocop-performance (1.
|
35
|
+
rubocop-performance (1.13.2)
|
36
36
|
rubocop (>= 1.7.0, < 2.0)
|
37
37
|
rubocop-ast (>= 0.4.0)
|
38
38
|
ruby-progressbar (1.11.0)
|
@@ -42,9 +42,9 @@ GEM
|
|
42
42
|
simplecov_json_formatter (~> 0.1)
|
43
43
|
simplecov-html (0.12.3)
|
44
44
|
simplecov_json_formatter (0.1.3)
|
45
|
-
standard (1.
|
46
|
-
rubocop (= 1.
|
47
|
-
rubocop-performance (= 1.
|
45
|
+
standard (1.7.0)
|
46
|
+
rubocop (= 1.25.0)
|
47
|
+
rubocop-performance (= 1.13.2)
|
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
|
765
|
+
```
|
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
|
699
780
|
```
|
700
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,10 @@ 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
|
+
|
708
816
|
## Acknowledgements
|
709
817
|
|
710
818
|
Mocktail is created & maintained by the software agency [Test
|
@@ -731,3 +839,4 @@ including (but not limited to) one-on-one communications, public posts/comments,
|
|
731
839
|
code reviews, pull requests, and GitHub issues. If violations occur, Test Double
|
732
840
|
will take any action they deem appropriate for the infraction, up to and
|
733
841
|
including blocking a user from the organization's repositories.
|
842
|
+
|
@@ -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,51 +22,50 @@ 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.
|
60
59
|
|
61
60
|
It has these mocked methods:
|
62
|
-
#{double.dry_methods.map { |method| " - #{method}" }.join("\n")}
|
61
|
+
#{double.dry_methods.sort.map { |method| " - #{method}" }.join("\n")}
|
63
62
|
|
64
|
-
#{double.dry_methods.map { |method| describe_dry_method(double_data, method) }.join("\n")}
|
63
|
+
#{double.dry_methods.sort.map { |method| describe_dry_method(double_data, method) }.join("\n")}
|
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
|
@@ -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,7 +11,7 @@ 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)}
|
@@ -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
|
@@ -1,65 +1,60 @@
|
|
1
|
-
# The top shelf stores all cross-thread & thread-aware state, so anything that
|
2
|
-
# goes here is on its own when it comes to ensuring thread safety.
|
3
1
|
module Mocktail
|
4
2
|
class TopShelf
|
5
3
|
def self.instance
|
6
|
-
|
4
|
+
Thread.current[:mocktail_top_shelf] ||= new
|
7
5
|
end
|
8
6
|
|
7
|
+
@@type_replacements = {}
|
8
|
+
@@type_replacements_mutex = Mutex.new
|
9
|
+
|
9
10
|
def initialize
|
10
|
-
@
|
11
|
-
@
|
12
|
-
@
|
13
|
-
@singleton_method_registrations = {}
|
11
|
+
@new_registrations = []
|
12
|
+
@of_next_registrations = []
|
13
|
+
@singleton_method_registrations = []
|
14
14
|
end
|
15
15
|
|
16
16
|
def type_replacement_for(type)
|
17
|
-
|
17
|
+
@@type_replacements_mutex.synchronize {
|
18
|
+
@@type_replacements[type] ||= TypeReplacement.new(type: type)
|
19
|
+
}
|
18
20
|
end
|
19
21
|
|
20
22
|
def type_replacement_if_exists_for(type)
|
21
|
-
|
23
|
+
@@type_replacements_mutex.synchronize {
|
24
|
+
@@type_replacements[type]
|
25
|
+
}
|
22
26
|
end
|
23
27
|
|
24
28
|
def reset_current_thread!
|
25
|
-
|
26
|
-
@of_next_registrations[Thread.current] = []
|
27
|
-
@singleton_method_registrations[Thread.current] = []
|
29
|
+
Thread.current[:mocktail_top_shelf] = self.class.new
|
28
30
|
end
|
29
31
|
|
30
32
|
def register_new_replacement!(type)
|
31
|
-
@new_registrations
|
32
|
-
@new_registrations[Thread.current] |= [type]
|
33
|
+
@new_registrations |= [type]
|
33
34
|
end
|
34
35
|
|
35
36
|
def new_replaced?(type)
|
36
|
-
@new_registrations
|
37
|
-
@new_registrations[Thread.current].include?(type)
|
37
|
+
@new_registrations.include?(type)
|
38
38
|
end
|
39
39
|
|
40
40
|
def register_of_next_replacement!(type)
|
41
|
-
@of_next_registrations
|
42
|
-
@of_next_registrations[Thread.current] |= [type]
|
41
|
+
@of_next_registrations |= [type]
|
43
42
|
end
|
44
43
|
|
45
44
|
def of_next_registered?(type)
|
46
|
-
@of_next_registrations
|
47
|
-
@of_next_registrations[Thread.current].include?(type)
|
45
|
+
@of_next_registrations.include?(type)
|
48
46
|
end
|
49
47
|
|
50
48
|
def unregister_of_next_replacement!(type)
|
51
|
-
@of_next_registrations
|
52
|
-
@of_next_registrations[Thread.current] -= [type]
|
49
|
+
@of_next_registrations -= [type]
|
53
50
|
end
|
54
51
|
|
55
52
|
def register_singleton_method_replacement!(type)
|
56
|
-
@singleton_method_registrations
|
57
|
-
@singleton_method_registrations[Thread.current] |= [type]
|
53
|
+
@singleton_method_registrations |= [type]
|
58
54
|
end
|
59
55
|
|
60
56
|
def singleton_methods_replaced?(type)
|
61
|
-
@singleton_method_registrations
|
62
|
-
@singleton_method_registrations[Thread.current].include?(type)
|
57
|
+
@singleton_method_registrations.include?(type)
|
63
58
|
end
|
64
59
|
end
|
65
60
|
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!
|
data/mocktail.gemspec
CHANGED
@@ -6,7 +6,7 @@ Gem::Specification.new do |spec|
|
|
6
6
|
spec.authors = ["Justin Searls"]
|
7
7
|
spec.email = ["searls@gmail.com"]
|
8
8
|
|
9
|
-
spec.summary = "your objects,
|
9
|
+
spec.summary = "Take your objects, and make them a double"
|
10
10
|
spec.homepage = "https://github.com/testdouble/mocktail"
|
11
11
|
spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
|
12
12
|
|
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.0
|
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-01-27 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,8 +117,8 @@ 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.6
|
120
121
|
signing_key:
|
121
122
|
specification_version: 4
|
122
|
-
summary: your objects,
|
123
|
+
summary: Take your objects, and make them a double
|
123
124
|
test_files: []
|
@@ -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
|