mocktail 0.0.2 → 0.0.6
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/CHANGELOG.md +28 -0
- data/Gemfile.lock +2 -2
- data/README.md +178 -18
- data/bin/console +21 -1
- data/lib/mocktail/explains_thing.rb +132 -0
- data/lib/mocktail/handles_dry_call/fulfills_stubbing/describes_unsatisfied_stubbing.rb +13 -0
- data/lib/mocktail/handles_dry_call/fulfills_stubbing.rb +4 -0
- data/lib/mocktail/handles_dry_call/validates_arguments.rb +2 -23
- data/lib/mocktail/imitates_type/makes_double/declares_dry_class.rb +29 -23
- data/lib/mocktail/imitates_type/makes_double/gathers_fakeable_instance_methods.rb +21 -0
- data/lib/mocktail/imitates_type/makes_double.rb +8 -4
- data/lib/mocktail/raises_neato_no_method_error.rb +81 -0
- data/lib/mocktail/share/creates_identifier.rb +28 -0
- data/lib/mocktail/{verifies_call/raises_verification_error → share}/stringifies_call.rb +16 -7
- data/lib/mocktail/share/stringifies_method_name.rb +11 -0
- data/lib/mocktail/simulates_argument_error/cleans_backtrace.rb +17 -0
- data/lib/mocktail/simulates_argument_error/reconciles_args_with_params.rb +20 -0
- data/lib/mocktail/simulates_argument_error/recreates_message.rb +29 -0
- data/lib/mocktail/simulates_argument_error/transforms_params.rb +32 -0
- data/lib/mocktail/simulates_argument_error.rb +30 -0
- data/lib/mocktail/value/cabinet.rb +12 -0
- data/lib/mocktail/value/double.rb +7 -8
- data/lib/mocktail/value/double_data.rb +10 -0
- data/lib/mocktail/value/explanation.rb +26 -0
- data/lib/mocktail/value/signature.rb +36 -0
- data/lib/mocktail/value/stub_returned_nil.rb +26 -0
- data/lib/mocktail/value/top_shelf.rb +24 -25
- data/lib/mocktail/value/type_replacement_data.rb +13 -0
- data/lib/mocktail/value/unsatisfied_stubbing.rb +8 -0
- data/lib/mocktail/value.rb +6 -0
- data/lib/mocktail/verifies_call/raises_verification_error.rb +4 -2
- data/lib/mocktail/version.rb +1 -1
- data/lib/mocktail.rb +9 -0
- data/mocktail.gemspec +1 -1
- metadata +22 -6
- data/lib/mocktail/share/simulates_argument_error.rb +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 67768c396af3e99d7ef7649d2afecf023f1dbdc0e361d960bebbf6befe908d3b
|
4
|
+
data.tar.gz: 196930b31afa82713bf68469c6d3cb434ce67f22f6cc4c7393c7c438201550c9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e8b1974f7c8068044c95b4113937aa5cd39972722244ff81bd60038cfe56ff2148a87c8b89bf5487e5b195fa25df1e23f31f667c618e2c375f27615e487d77f7
|
7
|
+
data.tar.gz: b0cf6ae9e2e1b7b2ce0d23144753d7dde66af999c110da1ac8815b2a95b4bdd6ad0ac1e7555fd4a1333bbc983811fd5fa32a052ea22bda01395a24dfb96eb6da
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,31 @@
|
|
1
|
+
# 0.0.6
|
2
|
+
|
3
|
+
* Require pathname, which I missed because `bundle exec` loads it. Wups!
|
4
|
+
|
5
|
+
# 0.0.5
|
6
|
+
|
7
|
+
* Fix concurrency [#6](https://github.com/testdouble/mocktail/pull/6)
|
8
|
+
|
9
|
+
# 0.0.4
|
10
|
+
|
11
|
+
* Introduce Mocktail.explain(), which will return a message & reference object
|
12
|
+
for any of:
|
13
|
+
* A class that has been passed to Mocktail.replace()
|
14
|
+
* An instance created by Mocktail.of() or of_next()
|
15
|
+
* A nil value returned by an unsatisfied stubbing invocation
|
16
|
+
* Fix some minor printing issue with the improved NoMethodError released in
|
17
|
+
0.0.3
|
18
|
+
|
19
|
+
|
20
|
+
# 0.0.3
|
21
|
+
|
22
|
+
* Implement method_missing on all mocked instance methods to print out useful
|
23
|
+
information, like the target type, the attempted call, an example method
|
24
|
+
definition that would match the call (for paint-by-numbers-like TDD), and
|
25
|
+
did_you_mean gem integration of similar method names in case it was just a
|
26
|
+
miss
|
27
|
+
* Cleans artificially-generated argument errors of gem-internal backtraces
|
28
|
+
|
1
29
|
# 0.0.2
|
2
30
|
|
3
31
|
* Drop Ruby 2.7 support. Unbeknownst to me (since I developed mocktail using
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -50,26 +50,37 @@ assert_equal "🎉", result
|
|
50
50
|
verify { glass.pour!(:a_drink) }
|
51
51
|
```
|
52
52
|
|
53
|
-
## Why
|
54
|
-
|
55
|
-
Besides
|
56
|
-
mocking libraries:
|
57
|
-
|
58
|
-
* **
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
* **
|
66
|
-
|
67
|
-
|
68
|
-
|
53
|
+
## Why Mocktail?
|
54
|
+
|
55
|
+
Besides helping you avoid a hangover, Mocktail offers several advantages over
|
56
|
+
Ruby's other mocking libraries:
|
57
|
+
|
58
|
+
* **Simpler test recipes**: [Mocktail.of_next(type)](#mocktailof_next) both
|
59
|
+
creates your mock and supplies to your subject under test in a single
|
60
|
+
one-liner. No more forcing dependency injection for the sake of your tests
|
61
|
+
* **WYSIWYG API**: Want to know how to stub a call to `phone.dial(911)`? You
|
62
|
+
just demonstrate the call with `stubs { phone.dial(911) }.with { :operator }`.
|
63
|
+
Because stubbing & verifying looks just like the actual call, your tests
|
64
|
+
becomes a sounding board for your APIs as you invent them
|
65
|
+
* **Argument validation**: Ever see a test pass after a change to a mocked
|
66
|
+
method should have broken it? Not with Mocktail, you haven't
|
67
|
+
* **Mocked class methods**: Singleton methods on modules and classes can be
|
68
|
+
faked out using [`Mocktail.replace(type)`](#mocktailreplace) without
|
69
|
+
sacrificing thread safety
|
70
|
+
* **Super-duper detailed error messages** A good mocking library should make
|
71
|
+
coding feel like
|
72
|
+
[paint-by-number](https://en.wikipedia.org/wiki/Paint_by_number), thoughtfully
|
73
|
+
guiding you from one step to the next. Calling a method that doesn't exist
|
74
|
+
will print a sample definition you can copy-paste. Verification failures will
|
75
|
+
print every call that _did_ occur. And [Mocktail.explain()](#mocktailexplain)
|
76
|
+
provides even more introspection
|
69
77
|
* **Expressive**: Built-in [argument matchers](#mocktailmatchers) and a simple
|
70
|
-
API for adding [custom matchers](#custom-matchers)
|
78
|
+
API for adding [custom matchers](#custom-matchers) allow you to tune your
|
79
|
+
stubbing configuration and call verification to match _exactly_ what your test
|
80
|
+
intends
|
71
81
|
* **Powerful**: [Argument captors](#mocktailcaptor) for assertions of very
|
72
|
-
complex arguments
|
82
|
+
complex arguments, as well as advanced configuration options for stubbing &
|
83
|
+
verification
|
73
84
|
|
74
85
|
## Ready to order?
|
75
86
|
|
@@ -539,6 +550,154 @@ down bugs. (If this concerns you, then the fact that class methods are
|
|
539
550
|
effectively global state may be a great reason not to rely too heavily on
|
540
551
|
them!)]
|
541
552
|
|
553
|
+
### Mocktail.explain
|
554
|
+
|
555
|
+
Test debugging is hard enough when there _aren't_ fake objects flying every
|
556
|
+
which way, so Mocktail tries to make it a little easier by way of better
|
557
|
+
messages throughout the library.
|
558
|
+
|
559
|
+
#### Undefined methods
|
560
|
+
|
561
|
+
One message you'll see automatically if you try to call a method
|
562
|
+
that doesn't exist is this one, which gives a sample definition of the method
|
563
|
+
you had attempted to call:
|
564
|
+
|
565
|
+
```ruby
|
566
|
+
class IceTray
|
567
|
+
end
|
568
|
+
|
569
|
+
ice_tray = Mocktail.of(IceTray)
|
570
|
+
|
571
|
+
ice_tray.fill(:water_type, 30)
|
572
|
+
# => No method `IceTray#fill' exists for call: (NoMethodError)
|
573
|
+
#
|
574
|
+
# fill(:water_type, 30)
|
575
|
+
#
|
576
|
+
# Need to define the method? Here's a sample definition:
|
577
|
+
#
|
578
|
+
# def fill(water_type, arg)
|
579
|
+
# end
|
580
|
+
```
|
581
|
+
|
582
|
+
From there, you can just copy-paste the provided method stub as a starting point
|
583
|
+
for your new method.
|
584
|
+
|
585
|
+
#### `nil` values returned by faked methods
|
586
|
+
|
587
|
+
Suppose you go ahead and implement the `fill` method above and configure a
|
588
|
+
stubbing:
|
589
|
+
|
590
|
+
```ruby
|
591
|
+
ice_tray = Mocktail.of(IceTray)
|
592
|
+
|
593
|
+
stubs { ice_tray.fill(:tap_water, 30) }.with { :normal_ice }
|
594
|
+
```
|
595
|
+
|
596
|
+
But then you find that your subject under test is just getting `nil` back and
|
597
|
+
you don't understand why:
|
598
|
+
|
599
|
+
```ruby
|
600
|
+
def prep
|
601
|
+
ice = ice_tray.fill(:tap_water, 50) # => nil
|
602
|
+
glass.add(ice)
|
603
|
+
end
|
604
|
+
```
|
605
|
+
|
606
|
+
You can pass that `nil` value to `Mocktail.explain` and get an
|
607
|
+
`UnsatisfiedStubExplanation` that will include both a `reference` object to explore
|
608
|
+
as well a summary message:
|
609
|
+
|
610
|
+
```ruby
|
611
|
+
def prep
|
612
|
+
ice = ice_tray.fill(:tap_water, 50).tap do |wat|
|
613
|
+
puts Mocktail.explain(wat).message
|
614
|
+
end
|
615
|
+
glass.add(ice)
|
616
|
+
end
|
617
|
+
```
|
618
|
+
|
619
|
+
Which will print:
|
620
|
+
|
621
|
+
```
|
622
|
+
This `nil' was returned by a mocked `IceTray#fill' method
|
623
|
+
because none of its configured stubbings were satisfied.
|
624
|
+
|
625
|
+
The actual call:
|
626
|
+
|
627
|
+
fill(:tap_water, 50)
|
628
|
+
|
629
|
+
Stubbings configured prior to this call but not satisfied by it:
|
630
|
+
|
631
|
+
fill(:tap_water, 30)
|
632
|
+
```
|
633
|
+
|
634
|
+
#### Fake instances created by Mocktail
|
635
|
+
|
636
|
+
Any instances created by `Mocktail.of` or `Mocktail.of_next` can also be passed
|
637
|
+
to `Mocktail.explain`, and they will list out all the calls and stubbings made
|
638
|
+
for each of their fake methods.
|
639
|
+
|
640
|
+
Calling `Mocktail.explain(ice_tray).message` following the example above will
|
641
|
+
yield:
|
642
|
+
|
643
|
+
```
|
644
|
+
This is a fake `IceTray' instance.
|
645
|
+
|
646
|
+
It has these mocked methods:
|
647
|
+
- fill
|
648
|
+
|
649
|
+
`IceTray#fill' stubbings:
|
650
|
+
|
651
|
+
fill(:tap_water, 30)
|
652
|
+
|
653
|
+
`IceTray#fill' calls:
|
654
|
+
|
655
|
+
fill(:tap_water, 50)
|
656
|
+
```
|
657
|
+
|
658
|
+
#### Modules and classes with singleton methods replaced
|
659
|
+
|
660
|
+
If you've called `Mocktail.replace()` on a class or module, it can also be
|
661
|
+
passed to `Mocktail.explain()` for a summary of all the stubbing configurations
|
662
|
+
and calls made against its faked singleton methods for the currently running
|
663
|
+
thread.
|
664
|
+
|
665
|
+
```ruby
|
666
|
+
Mocktail.replace(Shop)
|
667
|
+
|
668
|
+
stubs { |m| Shop.open!(m.numeric) }.with { :a_bar }
|
669
|
+
|
670
|
+
Shop.open!(42)
|
671
|
+
|
672
|
+
Shop.close!(42)
|
673
|
+
|
674
|
+
puts Mocktail.explain(Shop).message
|
675
|
+
```
|
676
|
+
|
677
|
+
Will print:
|
678
|
+
|
679
|
+
```ruby
|
680
|
+
`Shop' is a class that has had its singleton methods faked.
|
681
|
+
|
682
|
+
It has these mocked methods:
|
683
|
+
- close!
|
684
|
+
- open!
|
685
|
+
|
686
|
+
`Shop.close!' has no stubbings.
|
687
|
+
|
688
|
+
`Shop.close!' calls:
|
689
|
+
|
690
|
+
close!(42)
|
691
|
+
|
692
|
+
`Shop.open!' stubbings:
|
693
|
+
|
694
|
+
open!(numeric)
|
695
|
+
|
696
|
+
`Shop.open!' calls:
|
697
|
+
|
698
|
+
open!(42)
|
699
|
+
```
|
700
|
+
|
542
701
|
### Mocktail.reset
|
543
702
|
|
544
703
|
This one's simple: you probably want to call `Mocktail.reset` after each test,
|
@@ -572,3 +731,4 @@ including (but not limited to) one-on-one communications, public posts/comments,
|
|
572
731
|
code reviews, pull requests, and GitHub issues. If violations occur, Test Double
|
573
732
|
will take any action they deem appropriate for the infraction, up to and
|
574
733
|
including blocking a user from the organization's repositories.
|
734
|
+
|
data/bin/console
CHANGED
@@ -56,6 +56,26 @@ class Bartender
|
|
56
56
|
end
|
57
57
|
end
|
58
58
|
|
59
|
-
|
59
|
+
class IceTray
|
60
|
+
def fill(water_type, amount)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class Shop
|
65
|
+
def self.open!(bar_id)
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.close!(bar_id)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
Mocktail.replace(Shop)
|
73
|
+
|
74
|
+
stubs { |m| Shop.open!(m.numeric) }.with { :a_bar }
|
75
|
+
|
76
|
+
Shop.open!(42)
|
77
|
+
|
78
|
+
Shop.close!(42)
|
79
|
+
|
60
80
|
require "pry"
|
61
81
|
Pry.start
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require_relative "share/stringifies_method_name"
|
2
|
+
require_relative "share/stringifies_call"
|
3
|
+
|
4
|
+
module Mocktail
|
5
|
+
class ExplainsThing
|
6
|
+
def initialize
|
7
|
+
@stringifies_method_name = StringifiesMethodName.new
|
8
|
+
@stringifies_call = StringifiesCall.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def explain(thing)
|
12
|
+
if is_stub_returned_nil?(thing)
|
13
|
+
unsatisfied_stub_explanation(thing)
|
14
|
+
elsif (double = Mocktail.cabinet.double_for_instance(thing))
|
15
|
+
double_explanation(double)
|
16
|
+
elsif (type_replacement = TopShelf.instance.type_replacement_if_exists_for(thing))
|
17
|
+
replaced_type_explanation(type_replacement)
|
18
|
+
else
|
19
|
+
no_explanation(thing)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# Our fake nil doesn't even implement respond_to?, instead quacking like nil
|
26
|
+
def is_stub_returned_nil?(thing)
|
27
|
+
thing.was_returned_by_unsatisfied_stub?
|
28
|
+
rescue NoMethodError
|
29
|
+
end
|
30
|
+
|
31
|
+
def unsatisfied_stub_explanation(stub_returned_nil)
|
32
|
+
unsatisfied_stubbing = stub_returned_nil.unsatisfied_stubbing
|
33
|
+
dry_call = unsatisfied_stubbing.call
|
34
|
+
other_stubbings = unsatisfied_stubbing.other_stubbings
|
35
|
+
|
36
|
+
UnsatisfiedStubExplanation.new(unsatisfied_stubbing, <<~MSG)
|
37
|
+
This `nil' was returned by a mocked `#{@stringifies_method_name.stringify(dry_call)}' method
|
38
|
+
because none of its configured stubbings were satisfied.
|
39
|
+
|
40
|
+
The actual call:
|
41
|
+
|
42
|
+
#{@stringifies_call.stringify(dry_call, always_parens: true)}
|
43
|
+
|
44
|
+
#{describe_multiple_calls(other_stubbings.map(&:recording),
|
45
|
+
"Stubbings configured prior to this call but not satisfied by it",
|
46
|
+
"No stubbings were configured on this method")}
|
47
|
+
MSG
|
48
|
+
end
|
49
|
+
|
50
|
+
def double_explanation(double)
|
51
|
+
double_data = DoubleData.new(
|
52
|
+
type: double.original_type,
|
53
|
+
double: double.dry_instance,
|
54
|
+
calls: Mocktail.cabinet.calls_for_double(double),
|
55
|
+
stubbings: Mocktail.cabinet.stubbings_for_double(double)
|
56
|
+
)
|
57
|
+
|
58
|
+
DoubleExplanation.new(double_data, <<~MSG)
|
59
|
+
This is a fake `#{double.original_type.name}' instance.
|
60
|
+
|
61
|
+
It has these mocked methods:
|
62
|
+
#{double.dry_methods.sort.map { |method| " - #{method}" }.join("\n")}
|
63
|
+
|
64
|
+
#{double.dry_methods.sort.map { |method| describe_dry_method(double_data, method) }.join("\n")}
|
65
|
+
MSG
|
66
|
+
end
|
67
|
+
|
68
|
+
def replaced_type_explanation(type_replacement)
|
69
|
+
type_replacement_data = TypeReplacementData.new(
|
70
|
+
type: type_replacement.type,
|
71
|
+
replaced_method_names: type_replacement.replacement_methods.map(&:name).sort,
|
72
|
+
calls: Mocktail.cabinet.calls.select { |call|
|
73
|
+
call.double == type_replacement.type
|
74
|
+
},
|
75
|
+
stubbings: Mocktail.cabinet.stubbings.select { |stubbing|
|
76
|
+
stubbing.recording.double == type_replacement.type
|
77
|
+
}
|
78
|
+
)
|
79
|
+
|
80
|
+
ReplacedTypeExplanation.new(type_replacement_data, <<~MSG)
|
81
|
+
`#{type_replacement.type}' is a #{type_replacement.type.class.to_s.downcase} that has had its singleton methods faked.
|
82
|
+
|
83
|
+
It has these mocked methods:
|
84
|
+
#{type_replacement_data.replaced_method_names.map { |method| " - #{method}" }.join("\n")}
|
85
|
+
|
86
|
+
#{type_replacement_data.replaced_method_names.map { |method| describe_dry_method(type_replacement_data, method) }.join("\n")}
|
87
|
+
MSG
|
88
|
+
end
|
89
|
+
|
90
|
+
def describe_dry_method(double_data, method)
|
91
|
+
method_name = @stringifies_method_name.stringify(Call.new(
|
92
|
+
original_type: double_data.type,
|
93
|
+
singleton: double_data.type == double_data.double,
|
94
|
+
method: method
|
95
|
+
))
|
96
|
+
|
97
|
+
[
|
98
|
+
describe_multiple_calls(
|
99
|
+
double_data.stubbings.map(&:recording).select { |call|
|
100
|
+
call.method == method
|
101
|
+
},
|
102
|
+
"`#{method_name}' stubbings",
|
103
|
+
"`#{method_name}' has no stubbings"
|
104
|
+
),
|
105
|
+
describe_multiple_calls(
|
106
|
+
double_data.calls.select { |call|
|
107
|
+
call.method == method
|
108
|
+
},
|
109
|
+
"`#{method_name}' calls",
|
110
|
+
"`#{method_name}' has no calls"
|
111
|
+
)
|
112
|
+
].join("\n")
|
113
|
+
end
|
114
|
+
|
115
|
+
def describe_multiple_calls(calls, nonzero_message, zero_message)
|
116
|
+
if calls.empty?
|
117
|
+
"#{zero_message}.\n"
|
118
|
+
else
|
119
|
+
<<~MSG
|
120
|
+
#{nonzero_message}:
|
121
|
+
|
122
|
+
#{calls.map { |call| " " + @stringifies_call.stringify(call) }.join("\n\n")}
|
123
|
+
MSG
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def no_explanation(thing)
|
128
|
+
NoExplanation.new(thing,
|
129
|
+
"Unfortunately, Mocktail doesn't know what this thing is: #{thing.inspect}")
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Mocktail
|
2
|
+
class DescribesUnsatisfiedStubbing
|
3
|
+
def describe(dry_call)
|
4
|
+
UnsatisfiedStubbing.new(
|
5
|
+
call: dry_call,
|
6
|
+
other_stubbings: Mocktail.cabinet.stubbings.select { |stubbing|
|
7
|
+
dry_call.double == stubbing.recording.double &&
|
8
|
+
dry_call.method == stubbing.recording.method
|
9
|
+
}
|
10
|
+
)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -1,15 +1,19 @@
|
|
1
1
|
require_relative "fulfills_stubbing/finds_satisfaction"
|
2
|
+
require_relative "fulfills_stubbing/describes_unsatisfied_stubbing"
|
2
3
|
|
3
4
|
module Mocktail
|
4
5
|
class FulfillsStubbing
|
5
6
|
def initialize
|
6
7
|
@finds_satisfaction = FindsSatisfaction.new
|
8
|
+
@describes_unsatisfied_stubbing = DescribesUnsatisfiedStubbing.new
|
7
9
|
end
|
8
10
|
|
9
11
|
def fulfill(dry_call)
|
10
12
|
if (stubbing = satisfaction(dry_call))
|
11
13
|
stubbing.satisfied!
|
12
14
|
stubbing.effect&.call(dry_call)
|
15
|
+
else
|
16
|
+
StubReturnedNil.new(@describes_unsatisfied_stubbing.describe(dry_call))
|
13
17
|
end
|
14
18
|
end
|
15
19
|
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require_relative "../share/simulates_argument_error"
|
2
|
-
|
3
1
|
module Mocktail
|
4
2
|
class ValidatesArguments
|
5
3
|
def self.disable!
|
@@ -30,28 +28,9 @@ module Mocktail
|
|
30
28
|
def validate(dry_call)
|
31
29
|
return if self.class.disabled?
|
32
30
|
|
33
|
-
|
34
|
-
|
35
|
-
}.partition { |type, _|
|
36
|
-
[:req, :opt, :rest].include?(type)
|
37
|
-
}
|
38
|
-
|
39
|
-
unless args_match?(arg_params, dry_call.args) &&
|
40
|
-
kwargs_match?(kwarg_params, dry_call.kwargs)
|
41
|
-
raise @simulates_argument_error.simulate(arg_params, dry_call.args, kwarg_params, dry_call.kwargs)
|
31
|
+
if (error = @simulates_argument_error.simulate(dry_call))
|
32
|
+
raise error
|
42
33
|
end
|
43
34
|
end
|
44
|
-
|
45
|
-
private
|
46
|
-
|
47
|
-
def args_match?(arg_params, args)
|
48
|
-
args.size >= arg_params.count { |type, _| type == :req } &&
|
49
|
-
(arg_params.any? { |type, _| type == :rest } || args.size <= arg_params.size)
|
50
|
-
end
|
51
|
-
|
52
|
-
def kwargs_match?(kwarg_params, kwargs)
|
53
|
-
kwarg_params.select { |type, _| type == :keyreq }.all? { |_, name| kwargs.key?(name) } &&
|
54
|
-
(kwarg_params.any? { |type, _| type == :keyrest } || kwargs.keys.all? { |name| kwarg_params.any? { |_, key| name == key } })
|
55
|
-
end
|
56
35
|
end
|
57
36
|
end
|
@@ -2,13 +2,12 @@ module Mocktail
|
|
2
2
|
class DeclaresDryClass
|
3
3
|
def initialize
|
4
4
|
@handles_dry_call = HandlesDryCall.new
|
5
|
+
@raises_neato_no_method_error = RaisesNeatoNoMethodError.new
|
5
6
|
end
|
6
7
|
|
7
|
-
def declare(type)
|
8
|
-
type_type = type_of(type)
|
9
|
-
instance_methods = instance_methods_on(type)
|
8
|
+
def declare(type, instance_methods)
|
10
9
|
dry_class = Class.new(Object) {
|
11
|
-
include type if
|
10
|
+
include type if type.instance_of?(Module)
|
12
11
|
|
13
12
|
def initialize(*args, **kwargs, &blk)
|
14
13
|
end
|
@@ -18,15 +17,19 @@ module Mocktail
|
|
18
17
|
}
|
19
18
|
alias_method :kind_of?, :is_a?
|
20
19
|
|
21
|
-
if
|
20
|
+
if type.instance_of?(Class)
|
22
21
|
define_method :instance_of?, ->(thing) {
|
23
22
|
type == thing
|
24
23
|
}
|
25
24
|
end
|
26
25
|
}
|
27
26
|
|
28
|
-
|
29
|
-
|
27
|
+
# These have special implementations, but if the user defines
|
28
|
+
# any of them on the object itself, then they'll be replaced with normal
|
29
|
+
# mocked methods. YMMV
|
30
|
+
add_stringify_methods!(dry_class, :to_s, type, instance_methods)
|
31
|
+
add_stringify_methods!(dry_class, :inspect, type, instance_methods)
|
32
|
+
define_method_missing_errors!(dry_class, type, instance_methods)
|
30
33
|
|
31
34
|
define_double_methods!(dry_class, type, instance_methods)
|
32
35
|
|
@@ -43,7 +46,7 @@ module Mocktail
|
|
43
46
|
singleton: false,
|
44
47
|
double: self,
|
45
48
|
original_type: type,
|
46
|
-
dry_type:
|
49
|
+
dry_type: dry_class,
|
47
50
|
method: method,
|
48
51
|
original_method: type.instance_method(method),
|
49
52
|
args: args,
|
@@ -54,7 +57,7 @@ module Mocktail
|
|
54
57
|
end
|
55
58
|
end
|
56
59
|
|
57
|
-
def add_stringify_methods!(dry_class, method_name, type,
|
60
|
+
def add_stringify_methods!(dry_class, method_name, type, instance_methods)
|
58
61
|
dry_class.define_singleton_method method_name, -> {
|
59
62
|
if (id_matches = super().match(/:([0-9a-fx]+)>$/))
|
60
63
|
"#<Class #{"including module " if type.instance_of?(Module)}for mocktail of #{type.name}:#{id_matches[1]}>"
|
@@ -74,22 +77,25 @@ module Mocktail
|
|
74
77
|
end
|
75
78
|
end
|
76
79
|
|
77
|
-
def
|
78
|
-
if
|
79
|
-
:class
|
80
|
-
elsif type.is_a?(Module)
|
81
|
-
:module
|
82
|
-
end
|
83
|
-
end
|
80
|
+
def define_method_missing_errors!(dry_class, type, instance_methods)
|
81
|
+
return if instance_methods.include?(:method_missing)
|
84
82
|
|
85
|
-
|
86
|
-
|
87
|
-
|
83
|
+
raises_neato_no_method_error = @raises_neato_no_method_error
|
84
|
+
dry_class.define_method :method_missing, ->(name, *args, **kwargs, &block) {
|
85
|
+
raises_neato_no_method_error.call(
|
86
|
+
Call.new(
|
87
|
+
singleton: false,
|
88
|
+
double: self,
|
89
|
+
original_type: type,
|
90
|
+
dry_type: self.class,
|
91
|
+
method: name,
|
92
|
+
original_method: nil,
|
93
|
+
args: args,
|
94
|
+
kwargs: kwargs,
|
95
|
+
block: block
|
96
|
+
)
|
97
|
+
)
|
88
98
|
}
|
89
99
|
end
|
90
|
-
|
91
|
-
def ignored_ancestors
|
92
|
-
Object.ancestors
|
93
|
-
end
|
94
100
|
end
|
95
101
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Mocktail
|
2
|
+
class GathersFakeableInstanceMethods
|
3
|
+
def gather(type)
|
4
|
+
methods = type.instance_methods + [
|
5
|
+
(:respond_to_missing? if type.private_method_defined?(:respond_to_missing?))
|
6
|
+
].compact
|
7
|
+
|
8
|
+
methods.reject { |m|
|
9
|
+
ignore?(type, m)
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
def ignore?(type, method_name)
|
14
|
+
ignored_ancestors.include?(type.instance_method(method_name).owner)
|
15
|
+
end
|
16
|
+
|
17
|
+
def ignored_ancestors
|
18
|
+
Object.ancestors
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -1,17 +1,21 @@
|
|
1
1
|
require_relative "makes_double/declares_dry_class"
|
2
|
+
require_relative "makes_double/gathers_fakeable_instance_methods"
|
2
3
|
|
3
4
|
module Mocktail
|
4
5
|
class MakesDouble
|
5
6
|
def initialize
|
6
7
|
@declares_dry_class = DeclaresDryClass.new
|
8
|
+
@gathers_fakeable_instance_methods = GathersFakeableInstanceMethods.new
|
7
9
|
end
|
8
10
|
|
9
|
-
def make(
|
10
|
-
|
11
|
+
def make(type)
|
12
|
+
dry_methods = @gathers_fakeable_instance_methods.gather(type)
|
13
|
+
dry_type = @declares_dry_class.declare(type, dry_methods)
|
11
14
|
Double.new(
|
12
|
-
original_type:
|
15
|
+
original_type: type,
|
13
16
|
dry_type: dry_type,
|
14
|
-
dry_instance: dry_type.new
|
17
|
+
dry_instance: dry_type.new,
|
18
|
+
dry_methods: dry_methods
|
15
19
|
)
|
16
20
|
end
|
17
21
|
end
|