mocktail 1.0.0 → 1.1.2
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 +12 -0
- data/Gemfile.lock +16 -16
- data/README.md +183 -77
- data/lib/mocktail/debug.rb +49 -0
- data/lib/mocktail/explains_thing.rb +34 -4
- data/lib/mocktail/handles_dry_call/fulfills_stubbing/describes_unsatisfied_stubbing.rb +4 -2
- data/lib/mocktail/imitates_type/makes_double/declares_dry_class.rb +3 -0
- data/lib/mocktail/matchers/any.rb +3 -1
- data/lib/mocktail/matchers/numeric.rb +3 -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/share/compares_safely.rb +7 -0
- data/lib/mocktail/share/determines_matching_calls.rb +9 -3
- data/lib/mocktail/simulates_argument_error/transforms_params.rb +15 -9
- data/lib/mocktail/value/cabinet.rb +10 -3
- data/lib/mocktail/value/explanation.rb +3 -0
- data/lib/mocktail/value/fake_method_data.rb +9 -0
- data/lib/mocktail/value.rb +1 -0
- data/lib/mocktail/version.rb +1 -1
- data/lib/mocktail.rb +1 -0
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7eb64f90875e53a9bfd5b4d995f8526eb6695dd9bd8a88c850b02eed851cb96d
|
4
|
+
data.tar.gz: ae2b4443a3740c375c70e7e490cbe3463d9672f17665ae47930b6ee614e9891d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: de8d47071e93c4d406391a1155dea9b6a1a9ac89f56422ae29e6dda5f134c9e5e724ec8006aba833b5ff19d9d2dff4550384bc349506f5ab9cefe3fd2185af1d
|
7
|
+
data.tar.gz: 5bf88d0aadc08fe9a9380312cf491146439e72e335091e2e79849196b74e93aba20b0fac2d122152a4661008ee243b5b3b39cb6408fc776b2cc2567ffca0924f
|
data/.github/workflows/main.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,15 @@
|
|
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
|
+
|
1
13
|
# 1.0.0
|
2
14
|
|
3
15
|
* First breaking change! 🎉
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
mocktail (1.
|
4
|
+
mocktail (1.1.2)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
@@ -11,28 +11,28 @@ GEM
|
|
11
11
|
docile (1.4.0)
|
12
12
|
method_source (1.0.0)
|
13
13
|
minitest (5.15.0)
|
14
|
-
parallel (1.
|
15
|
-
parser (3.
|
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
@@ -10,6 +10,11 @@ library for Ruby that provides a terse and robust API for creating mocks,
|
|
10
10
|
getting them in the hands of the code you're testing, stub & verify behavior,
|
11
11
|
and even safely override class methods.
|
12
12
|
|
13
|
+
If you'd prefer a voice & video introduction to Mocktail aside from this README,
|
14
|
+
you might enjoy this ⚡️[Lightning
|
15
|
+
Talk](https://blog.testdouble.com/talks/2022-05-18-please-mock-me?utm_source=twitter&utm_medium=organic-social&utm_campaign=conf-talk)⚡️
|
16
|
+
from RailsConf 2022.
|
17
|
+
|
13
18
|
## An aperitif
|
14
19
|
|
15
20
|
Before getting into the details, let's demonstrate what Mocktail's API looks
|
@@ -553,14 +558,160 @@ them!)]
|
|
553
558
|
### Mocktail.explain
|
554
559
|
|
555
560
|
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
|
561
|
+
which way, so Mocktail tries to make it a little easier on you. In addition to
|
562
|
+
returning useful messages throughout the API, the gem also includes an
|
563
|
+
introspection method `Mocktail.explain(thing)`, which returns a human-readable
|
564
|
+
`message` and a `reference` object with useful attributes (that vary depending
|
565
|
+
on the type of fake `thing` you pass in. Below are some things `explain()` can
|
566
|
+
do.
|
567
|
+
|
568
|
+
#### Fake instances created by Mocktail
|
569
|
+
|
570
|
+
Any instances created by `Mocktail.of` or `Mocktail.of_next` can be passed to
|
571
|
+
`Mocktail.explain`, and they will list out all the calls and stubbings made for
|
572
|
+
each of their fake methods.
|
573
|
+
|
574
|
+
Suppose these interactions have occurred:
|
575
|
+
|
576
|
+
```ruby
|
577
|
+
ice_tray = Mocktail.of(IceTray)
|
578
|
+
|
579
|
+
Mocktail.stubs { ice_tray.fill(:tap_water, 30) }.with { :some_ice }
|
580
|
+
|
581
|
+
ice_tray.fill(:tap_water, 50)
|
582
|
+
```
|
583
|
+
|
584
|
+
You can interrogate what's going on with the fake instance by passing it to
|
585
|
+
`explain`:
|
586
|
+
|
587
|
+
```ruby
|
588
|
+
explanation = Mocktail.explain(ice_tray)
|
589
|
+
|
590
|
+
explanation.reference.type #=> IceTray
|
591
|
+
explanation.reference.double #=> The ice_tray instance
|
592
|
+
explanation.reference.calls #=> details on each invocation of each method
|
593
|
+
explanation.reference.stubbings #=> all stubbings configured for each method
|
594
|
+
```
|
595
|
+
|
596
|
+
Calling `explanation.message` will return:
|
597
|
+
|
598
|
+
```
|
599
|
+
This is a fake `IceTray' instance.
|
600
|
+
|
601
|
+
It has these mocked methods:
|
602
|
+
- fill
|
603
|
+
|
604
|
+
`IceTray#fill' stubbings:
|
605
|
+
|
606
|
+
fill(:tap_water, 30)
|
607
|
+
|
608
|
+
`IceTray#fill' calls:
|
609
|
+
|
610
|
+
fill(:tap_water, 50)
|
611
|
+
|
612
|
+
```
|
613
|
+
|
614
|
+
#### Modules and classes with singleton methods replaced
|
615
|
+
|
616
|
+
If you've called `Mocktail.replace()` on a class or module, it can also be
|
617
|
+
passed to `Mocktail.explain()` for a summary of all the stubbing configurations
|
618
|
+
and calls made against its faked singleton methods for the currently running
|
619
|
+
thread.
|
620
|
+
|
621
|
+
Imagine a `Shop` class with `self.open!` and `self.close!` singleton methods:
|
622
|
+
|
623
|
+
```ruby
|
624
|
+
Mocktail.replace(Shop)
|
625
|
+
|
626
|
+
stubs { |m| Shop.open!(m.numeric) }.with { :a_bar }
|
627
|
+
|
628
|
+
Shop.open!(42)
|
629
|
+
|
630
|
+
Shop.close!(42)
|
631
|
+
|
632
|
+
explanation = Mocktail.explain(Shop)
|
633
|
+
|
634
|
+
explanation.reference.type #=> Shop
|
635
|
+
explanation.reference.replaced_method_names #=> [:close!, :open!]
|
636
|
+
explanation.reference.calls #=> details on each invocation of each method
|
637
|
+
explanation.reference.stubbings #=> all stubbings configured for each method
|
638
|
+
```
|
639
|
+
|
640
|
+
And `explanation.message` will return:
|
641
|
+
|
642
|
+
```ruby
|
643
|
+
`Shop' is a class that has had its singleton methods faked.
|
644
|
+
|
645
|
+
It has these mocked methods:
|
646
|
+
- close!
|
647
|
+
- open!
|
648
|
+
|
649
|
+
`Shop.close!' has no stubbings.
|
650
|
+
|
651
|
+
`Shop.close!' calls:
|
652
|
+
|
653
|
+
close!(42)
|
654
|
+
|
655
|
+
close!(42)
|
656
|
+
|
657
|
+
`Shop.open!' stubbings:
|
658
|
+
|
659
|
+
open!(numeric)
|
660
|
+
|
661
|
+
open!(numeric)
|
662
|
+
|
663
|
+
`Shop.open!' calls:
|
664
|
+
|
665
|
+
open!(42)
|
666
|
+
|
667
|
+
open!(42)
|
668
|
+
```
|
669
|
+
|
670
|
+
#### Methods on faked instances and replaced types
|
671
|
+
|
672
|
+
In addition to passing the test double, you can also pass a reference to any
|
673
|
+
fake method created by Mocktail to `Mocktail.explain`:
|
674
|
+
|
675
|
+
```ruby
|
676
|
+
ice_tray = Mocktail.of(IceTray)
|
677
|
+
|
678
|
+
ice_tray.fill(:chilled, 50)
|
679
|
+
|
680
|
+
explanation = Mocktail.explain(ice_tray.method(:fill))
|
681
|
+
|
682
|
+
explanation.reference.receiver #=> a reference to the `ice_tray` instance
|
683
|
+
explanation.reference.calls #=> details on each invocation of the method
|
684
|
+
explanation.reference.stubbings #=> all stubbings configured for the method
|
685
|
+
```
|
686
|
+
|
687
|
+
The above may be handy in cases where you want to assert the number of calls of
|
688
|
+
a method outside the `Mocktail.verify` API:
|
689
|
+
|
690
|
+
```ruby
|
691
|
+
assert_equal 1, explanation.reference.calls.size
|
692
|
+
```
|
693
|
+
|
694
|
+
The explanation will also contain a `message` like this:
|
695
|
+
|
696
|
+
```
|
697
|
+
`IceTray#fill' has no stubbings.
|
698
|
+
|
699
|
+
`IceTray#fill' calls:
|
700
|
+
|
701
|
+
fill(:chilled, 50)
|
702
|
+
```
|
703
|
+
|
704
|
+
Replaced singleton methods can also be passed to `explain()`, so something like
|
705
|
+
`Mocktail.explain(Shop.method(:open!))` from the earlier example would also work
|
706
|
+
(with `Shop` being the `receiver` on the explanation's `reference`).
|
558
707
|
|
559
708
|
#### Undefined methods
|
560
709
|
|
561
|
-
|
562
|
-
that
|
563
|
-
|
710
|
+
There's no API for this one, but Mocktail also offers explanations for methods
|
711
|
+
that don't exist yet. You'll see this error message whenever you try to call a
|
712
|
+
method that doesn't exist on a test double. The message is designed to
|
713
|
+
facilitate "paint-by-numbers" TDD, by including a sample definition of the
|
714
|
+
method you had attempted to call that can be copy-pasted into a source listing:
|
564
715
|
|
565
716
|
```ruby
|
566
717
|
class IceTray
|
@@ -589,7 +740,7 @@ class IceTray
|
|
589
740
|
end
|
590
741
|
```
|
591
742
|
|
592
|
-
###
|
743
|
+
### Mocktail.explain_nils
|
593
744
|
|
594
745
|
Is a faked method returning `nil` and you don't understand why?
|
595
746
|
|
@@ -656,75 +807,6 @@ The `reference` object will have details of the `call` itself, an array of
|
|
656
807
|
`other_stubbings` defined on the faked method, and a `backtrace` to determine
|
657
808
|
which call site produced the unexpected `nil` value.
|
658
809
|
|
659
|
-
#### Fake instances created by Mocktail
|
660
|
-
|
661
|
-
Any instances created by `Mocktail.of` or `Mocktail.of_next` can be passed to
|
662
|
-
`Mocktail.explain`, and they will list out all the calls and stubbings made for
|
663
|
-
each of their fake methods.
|
664
|
-
|
665
|
-
Calling `Mocktail.explain(ice_tray).message` following the example above will
|
666
|
-
yield:
|
667
|
-
|
668
|
-
```
|
669
|
-
This is a fake `IceTray' instance.
|
670
|
-
|
671
|
-
It has these mocked methods:
|
672
|
-
- fill
|
673
|
-
|
674
|
-
`IceTray#fill' stubbings:
|
675
|
-
|
676
|
-
fill(:tap_water, 30)
|
677
|
-
|
678
|
-
`IceTray#fill' calls:
|
679
|
-
|
680
|
-
fill(:tap_water, 50)
|
681
|
-
```
|
682
|
-
|
683
|
-
#### Modules and classes with singleton methods replaced
|
684
|
-
|
685
|
-
If you've called `Mocktail.replace()` on a class or module, it can also be
|
686
|
-
passed to `Mocktail.explain()` for a summary of all the stubbing configurations
|
687
|
-
and calls made against its faked singleton methods for the currently running
|
688
|
-
thread.
|
689
|
-
|
690
|
-
Imagine a `Shop` class with `self.open!` and `self.close!` singleton methods:
|
691
|
-
|
692
|
-
```ruby
|
693
|
-
Mocktail.replace(Shop)
|
694
|
-
|
695
|
-
stubs { |m| Shop.open!(m.numeric) }.with { :a_bar }
|
696
|
-
|
697
|
-
Shop.open!(42)
|
698
|
-
|
699
|
-
Shop.close!(42)
|
700
|
-
|
701
|
-
puts Mocktail.explain(Shop).message
|
702
|
-
```
|
703
|
-
|
704
|
-
Will print:
|
705
|
-
|
706
|
-
```ruby
|
707
|
-
`Shop' is a class that has had its singleton methods faked.
|
708
|
-
|
709
|
-
It has these mocked methods:
|
710
|
-
- close!
|
711
|
-
- open!
|
712
|
-
|
713
|
-
`Shop.close!' has no stubbings.
|
714
|
-
|
715
|
-
`Shop.close!' calls:
|
716
|
-
|
717
|
-
close!(42)
|
718
|
-
|
719
|
-
`Shop.open!' stubbings:
|
720
|
-
|
721
|
-
open!(numeric)
|
722
|
-
|
723
|
-
`Shop.open!' calls:
|
724
|
-
|
725
|
-
open!(42)
|
726
|
-
```
|
727
|
-
|
728
810
|
### Mocktail.reset
|
729
811
|
|
730
812
|
This one's simple: you probably want to call `Mocktail.reset` after each test,
|
@@ -736,10 +818,35 @@ Calling reset in a `teardown` or `after(:each)` hook will also improve the
|
|
736
818
|
usefulness of messages returned by `Mocktail.explain` and
|
737
819
|
`Mocktail.explain_nils`.
|
738
820
|
|
821
|
+
## References
|
822
|
+
|
823
|
+
Mocktail is designed following a somewhat academic understanding of what mocking
|
824
|
+
is and how it should be used. Below are several references on this topic.
|
825
|
+
|
826
|
+
Blog Posts and Papers:
|
827
|
+
|
828
|
+
- [Endo-Testing: Unit Testing with Mock
|
829
|
+
Objects](<https://www2.ccs.neu.edu/research/demeter/related-work/extreme-programming/MockObjectsFinal.PDF>
|
830
|
+
by Tim Mackinnon, Steve Freeman, and Philip Craig, the paper that introduced
|
831
|
+
mocking presented by the creators of mocking.
|
832
|
+
- Michael Feathers' [The Flawed Theory Behind Unit
|
833
|
+
Testing](<https://michaelfeathers.typepad.com/michael_feathers_blog/2008/06/the-flawed-theo.html>)
|
834
|
+
|
835
|
+
Books:
|
836
|
+
|
837
|
+
- [_Growing Object-Oriented Software, Guided by
|
838
|
+
Tests_](<https://bookshop.org/books/growing-object-oriented-software-guided-by-tests/9780321503626>)
|
839
|
+
by Steve Freeman and Nat Price
|
840
|
+
|
841
|
+
Talks:
|
842
|
+
|
843
|
+
- [Please don’t mock me](https://www.youtube.com/watch?v=Af4M8GMoxi4) by Justin
|
844
|
+
Searls
|
845
|
+
|
739
846
|
## Acknowledgements
|
740
847
|
|
741
848
|
Mocktail is created & maintained by the software agency [Test
|
742
|
-
Double](https://
|
849
|
+
Double](https://testdouble.com). If you've ever come across our eponymously-named
|
743
850
|
[testdouble.js](https://github.com/testdouble/testdouble.js/), you might find
|
744
851
|
Mocktail's API to be quite similar. The term "test double" was originally coined
|
745
852
|
by Gerard Meszaros in his book [xUnit Test
|
@@ -762,4 +869,3 @@ including (but not limited to) one-on-one communications, public posts/comments,
|
|
762
869
|
code reviews, pull requests, and GitHub issues. If violations occur, Test Double
|
763
870
|
will take any action they deem appropriate for the infraction, up to and
|
764
871
|
including blocking a user from the organization's repositories.
|
765
|
-
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Mocktail
|
2
|
+
module Debug
|
3
|
+
# It would be easy and bad for the mocktail lib to call something like
|
4
|
+
#
|
5
|
+
# double == other_double
|
6
|
+
#
|
7
|
+
# But if it's a double, that means anyone who stubs that method could change
|
8
|
+
# the internal behavior of the library in unexpected ways (as happened here:
|
9
|
+
# https://github.com/testdouble/mocktail/issues/7 )
|
10
|
+
#
|
11
|
+
# For that reason when we run our tests, we also want to blow up if this
|
12
|
+
# happens unintentionally. This works in conjunction with the test
|
13
|
+
# MockingMethodfulClassesTest, because it mocks every defined method on the
|
14
|
+
# mocked BasicObject
|
15
|
+
def self.guard_against_mocktail_accidentally_calling_mocks_if_debugging!
|
16
|
+
return unless ENV["MOCKTAIL_DEBUG_ACCIDENTAL_INTERNAL_MOCK_CALLS"]
|
17
|
+
raise
|
18
|
+
rescue => e
|
19
|
+
base_path = Pathname.new(__FILE__).dirname.to_s
|
20
|
+
backtrace_minus_this_and_whoever_called_this = e.backtrace[2..]
|
21
|
+
internal_call_sites = backtrace_minus_this_and_whoever_called_this.take_while { |call_site|
|
22
|
+
# the "in `block" is very confusing but necessary to include lines after
|
23
|
+
# a stubs { blah.foo }.with { … } call, since that's when most of the
|
24
|
+
# good stuff happens
|
25
|
+
call_site.start_with?(base_path) || call_site.include?("in `block")
|
26
|
+
}.reject { |call_site| call_site.include?("in `block") }
|
27
|
+
|
28
|
+
approved_call_sites = [
|
29
|
+
"fulfills_stubbing.rb:14",
|
30
|
+
"validates_arguments.rb:16",
|
31
|
+
"validates_arguments.rb:19"
|
32
|
+
]
|
33
|
+
if internal_call_sites.any? && approved_call_sites.none? { |approved_call_site|
|
34
|
+
internal_call_sites.first.include?(approved_call_site)
|
35
|
+
}
|
36
|
+
raise Error.new <<~MSG
|
37
|
+
Unauthorized internal call of a mock internally by Mocktail itself:
|
38
|
+
|
39
|
+
#{internal_call_sites.first}
|
40
|
+
|
41
|
+
Offending call's complete stack trace:
|
42
|
+
|
43
|
+
#{backtrace_minus_this_and_whoever_called_this.join("\n")}
|
44
|
+
==END OFFENDING TRACE==
|
45
|
+
MSG
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -13,6 +13,8 @@ module Mocktail
|
|
13
13
|
double_explanation(double)
|
14
14
|
elsif (type_replacement = TopShelf.instance.type_replacement_if_exists_for(thing))
|
15
15
|
replaced_type_explanation(type_replacement)
|
16
|
+
elsif (fake_method_explanation = fake_method_explanation_for(thing))
|
17
|
+
fake_method_explanation
|
16
18
|
else
|
17
19
|
no_explanation(thing)
|
18
20
|
end
|
@@ -20,13 +22,37 @@ module Mocktail
|
|
20
22
|
|
21
23
|
private
|
22
24
|
|
23
|
-
def
|
24
|
-
|
25
|
+
def fake_method_explanation_for(thing)
|
26
|
+
return unless thing.is_a?(Method)
|
27
|
+
method = thing
|
28
|
+
receiver = thing.receiver
|
29
|
+
|
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
|
35
|
+
|
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
|
43
|
+
end
|
44
|
+
|
45
|
+
def data_for_double(double)
|
46
|
+
DoubleData.new(
|
25
47
|
type: double.original_type,
|
26
48
|
double: double.dry_instance,
|
27
49
|
calls: Mocktail.cabinet.calls_for_double(double),
|
28
50
|
stubbings: Mocktail.cabinet.stubbings_for_double(double)
|
29
51
|
)
|
52
|
+
end
|
53
|
+
|
54
|
+
def double_explanation(double)
|
55
|
+
double_data = data_for_double(double)
|
30
56
|
|
31
57
|
DoubleExplanation.new(double_data, <<~MSG)
|
32
58
|
This is a fake `#{double.original_type.name}' instance.
|
@@ -38,8 +64,8 @@ module Mocktail
|
|
38
64
|
MSG
|
39
65
|
end
|
40
66
|
|
41
|
-
def
|
42
|
-
|
67
|
+
def data_for_type_replacement(type_replacement)
|
68
|
+
TypeReplacementData.new(
|
43
69
|
type: type_replacement.type,
|
44
70
|
replaced_method_names: type_replacement.replacement_methods.map(&:name).sort,
|
45
71
|
calls: Mocktail.cabinet.calls.select { |call|
|
@@ -49,6 +75,10 @@ module Mocktail
|
|
49
75
|
stubbing.recording.double == type_replacement.type
|
50
76
|
}
|
51
77
|
)
|
78
|
+
end
|
79
|
+
|
80
|
+
def replaced_type_explanation(type_replacement)
|
81
|
+
type_replacement_data = data_for_type_replacement(type_replacement)
|
52
82
|
|
53
83
|
ReplacedTypeExplanation.new(type_replacement_data, <<~MSG)
|
54
84
|
`#{type_replacement.type}' is a #{type_replacement.type.class.to_s.downcase} that has had its singleton methods faked.
|
@@ -1,17 +1,19 @@
|
|
1
1
|
require_relative "../../share/cleans_backtrace"
|
2
|
+
require_relative "../../share/compares_safely"
|
2
3
|
|
3
4
|
module Mocktail
|
4
5
|
class DescribesUnsatisfiedStubbing
|
5
6
|
def initialize
|
6
7
|
@cleans_backtrace = CleansBacktrace.new
|
8
|
+
@compares_safely = ComparesSafely.new
|
7
9
|
end
|
8
10
|
|
9
11
|
def describe(dry_call)
|
10
12
|
UnsatisfyingCall.new(
|
11
13
|
call: dry_call,
|
12
14
|
other_stubbings: Mocktail.cabinet.stubbings.select { |stubbing|
|
13
|
-
dry_call.double
|
14
|
-
dry_call.method
|
15
|
+
@compares_safely.compare(dry_call.double, stubbing.recording.double) &&
|
16
|
+
@compares_safely.compare(dry_call.method, stubbing.recording.method)
|
15
17
|
},
|
16
18
|
backtrace: @cleans_backtrace.clean(Error.new).backtrace
|
17
19
|
)
|
@@ -41,7 +41,10 @@ module Mocktail
|
|
41
41
|
def define_double_methods!(dry_class, type, instance_methods)
|
42
42
|
handles_dry_call = @handles_dry_call
|
43
43
|
instance_methods.each do |method|
|
44
|
+
dry_class.undef_method(method) if dry_class.method_defined?(method)
|
45
|
+
|
44
46
|
dry_class.define_method method, ->(*args, **kwargs, &block) {
|
47
|
+
Debug.guard_against_mocktail_accidentally_calling_mocks_if_debugging!
|
45
48
|
handles_dry_call.handle(Call.new(
|
46
49
|
singleton: false,
|
47
50
|
double: self,
|
@@ -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)
|
@@ -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
|
@@ -1,8 +1,14 @@
|
|
1
|
+
require_relative "compares_safely"
|
2
|
+
|
1
3
|
module Mocktail
|
2
4
|
class DeterminesMatchingCalls
|
5
|
+
def initialize
|
6
|
+
@compares_safely = ComparesSafely.new
|
7
|
+
end
|
8
|
+
|
3
9
|
def determine(real_call, demo_call, demo_config)
|
4
|
-
real_call.double
|
5
|
-
real_call.method
|
10
|
+
@compares_safely.compare(real_call.double, demo_call.double) &&
|
11
|
+
@compares_safely.compare(real_call.method, demo_call.method) &&
|
6
12
|
|
7
13
|
# Matcher implementation will replace this:
|
8
14
|
args_match?(real_call.args, demo_call.args, demo_config.ignore_extra_args) &&
|
@@ -53,7 +59,7 @@ module Mocktail
|
|
53
59
|
demo_arg.is_mocktail_matcher?
|
54
60
|
demo_arg.match?(real_arg)
|
55
61
|
else
|
56
|
-
demo_arg == real_arg
|
62
|
+
demo_arg == real_arg # TODO <-- test if mock object and call safe compare if so, otherwise ==
|
57
63
|
end
|
58
64
|
end
|
59
65
|
end
|
@@ -1,16 +1,22 @@
|
|
1
|
+
require_relative "../share/compares_safely"
|
2
|
+
|
1
3
|
module Mocktail
|
2
4
|
class TransformsParams
|
5
|
+
def initialize
|
6
|
+
@compares_safely = ComparesSafely.new
|
7
|
+
end
|
8
|
+
|
3
9
|
def transform(dry_call)
|
4
10
|
params = dry_call.original_method.parameters
|
5
11
|
|
6
12
|
Signature.new(
|
7
13
|
positional_params: Params.new(
|
8
|
-
all: params.select { |
|
9
|
-
[:req, :opt, :rest].
|
14
|
+
all: params.select { |t, _|
|
15
|
+
[:req, :opt, :rest].any? { |param_type| @compares_safely.compare(t, param_type) }
|
10
16
|
}.map { |_, name| name },
|
11
|
-
required: params.select { |t, _| t
|
12
|
-
optional: params.select { |t, _| t
|
13
|
-
rest: params.find { |
|
17
|
+
required: params.select { |t, _| @compares_safely.compare(t, :req) }.map { |_, n| n },
|
18
|
+
optional: params.select { |t, _| @compares_safely.compare(t, :opt) }.map { |_, n| n },
|
19
|
+
rest: params.find { |t, _| @compares_safely.compare(t, :rest) } & [1]
|
14
20
|
),
|
15
21
|
positional_args: dry_call.args,
|
16
22
|
|
@@ -18,13 +24,13 @@ module Mocktail
|
|
18
24
|
all: params.select { |type, _|
|
19
25
|
[:keyreq, :key, :keyrest].include?(type)
|
20
26
|
}.map { |_, name| name },
|
21
|
-
required: params.select { |t, _| t
|
22
|
-
optional: params.select { |t, _| t
|
23
|
-
rest: params.find { |
|
27
|
+
required: params.select { |t, _| @compares_safely.compare(t, :keyreq) }.map { |_, n| n },
|
28
|
+
optional: params.select { |t, _| @compares_safely.compare(t, :key) }.map { |_, n| n },
|
29
|
+
rest: params.find { |t, _| @compares_safely.compare(t, :keyrest) } & [1]
|
24
30
|
),
|
25
31
|
keyword_args: dry_call.kwargs,
|
26
32
|
|
27
|
-
block_param: params.find { |
|
33
|
+
block_param: params.find { |t, _| @compares_safely.compare(t, :block) } & [1],
|
28
34
|
block_arg: dry_call.block
|
29
35
|
)
|
30
36
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require_relative "../share/compares_safely"
|
2
|
+
|
1
3
|
# The Cabinet stores all thread-local state, so anything that goes here
|
2
4
|
# is guaranteed by Mocktail to be local to the currently-running thread
|
3
5
|
module Mocktail
|
@@ -6,6 +8,7 @@ module Mocktail
|
|
6
8
|
attr_reader :calls, :stubbings, :unsatisfying_calls
|
7
9
|
|
8
10
|
def initialize
|
11
|
+
@compares_safely = ComparesSafely.new
|
9
12
|
@doubles = []
|
10
13
|
@calls = []
|
11
14
|
@stubbings = []
|
@@ -45,15 +48,19 @@ module Mocktail
|
|
45
48
|
end
|
46
49
|
|
47
50
|
def double_for_instance(thing)
|
48
|
-
@doubles.find { |double| double.dry_instance
|
51
|
+
@doubles.find { |double| @compares_safely.compare(double.dry_instance, thing) }
|
49
52
|
end
|
50
53
|
|
51
54
|
def stubbings_for_double(double)
|
52
|
-
@stubbings.select { |stubbing|
|
55
|
+
@stubbings.select { |stubbing|
|
56
|
+
@compares_safely.compare(stubbing.recording.double, double.dry_instance)
|
57
|
+
}
|
53
58
|
end
|
54
59
|
|
55
60
|
def calls_for_double(double)
|
56
|
-
@calls.select { |call|
|
61
|
+
@calls.select { |call|
|
62
|
+
@compares_safely.compare(call.double, double.dry_instance)
|
63
|
+
}
|
57
64
|
end
|
58
65
|
end
|
59
66
|
end
|
data/lib/mocktail/value.rb
CHANGED
@@ -4,6 +4,7 @@ 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"
|
data/lib/mocktail/version.rb
CHANGED
data/lib/mocktail.rb
CHANGED
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: 1.
|
4
|
+
version: 1.1.2
|
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-06-25 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -29,6 +29,7 @@ files:
|
|
29
29
|
- bin/console
|
30
30
|
- bin/setup
|
31
31
|
- lib/mocktail.rb
|
32
|
+
- lib/mocktail/debug.rb
|
32
33
|
- lib/mocktail/dsl.rb
|
33
34
|
- lib/mocktail/errors.rb
|
34
35
|
- lib/mocktail/explains_nils.rb
|
@@ -67,6 +68,7 @@ files:
|
|
67
68
|
- lib/mocktail/replaces_type/redefines_singleton_methods.rb
|
68
69
|
- lib/mocktail/resets_state.rb
|
69
70
|
- lib/mocktail/share/cleans_backtrace.rb
|
71
|
+
- lib/mocktail/share/compares_safely.rb
|
70
72
|
- lib/mocktail/share/creates_identifier.rb
|
71
73
|
- lib/mocktail/share/determines_matching_calls.rb
|
72
74
|
- lib/mocktail/share/stringifies_call.rb
|
@@ -82,6 +84,7 @@ files:
|
|
82
84
|
- lib/mocktail/value/double.rb
|
83
85
|
- lib/mocktail/value/double_data.rb
|
84
86
|
- lib/mocktail/value/explanation.rb
|
87
|
+
- lib/mocktail/value/fake_method_data.rb
|
85
88
|
- lib/mocktail/value/matcher_registry.rb
|
86
89
|
- lib/mocktail/value/signature.rb
|
87
90
|
- lib/mocktail/value/stubbing.rb
|
@@ -116,7 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
116
119
|
- !ruby/object:Gem::Version
|
117
120
|
version: '0'
|
118
121
|
requirements: []
|
119
|
-
rubygems_version: 3.
|
122
|
+
rubygems_version: 3.3.6
|
120
123
|
signing_key:
|
121
124
|
specification_version: 4
|
122
125
|
summary: Take your objects, and make them a double
|