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