mocktail 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/Gemfile.lock +1 -1
- data/README.md +177 -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/imitates_type/makes_double/declares_dry_class.rb +1 -20
- 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 +3 -1
- data/lib/mocktail/share/creates_identifier.rb +17 -2
- data/lib/mocktail/share/stringifies_method_name.rb +11 -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/stub_returned_nil.rb +26 -0
- data/lib/mocktail/value/top_shelf.rb +4 -0
- data/lib/mocktail/value/type_replacement_data.rb +13 -0
- data/lib/mocktail/value/unsatisfied_stubbing.rb +8 -0
- data/lib/mocktail/value.rb +5 -0
- data/lib/mocktail/verifies_call/raises_verification_error.rb +3 -1
- data/lib/mocktail/version.rb +1 -1
- data/lib/mocktail.rb +7 -0
- metadata +11 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 426f5dbb08f6db2542f0a323d186df7126aaae652e848b0a0c378a602231d777
|
4
|
+
data.tar.gz: 898501920257accc975d441a12d9e54a034c40aef8c950be2f0fa80aeb0288c6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6eea3625f3851c035e00c1593e91ae7d97c76b499510a71bd8b68fb0cfb27c4cac9116f9224f30853a97cfb6826385338506e33bf2ea6d43160f12093e8a591c
|
7
|
+
data.tar.gz: 1e6041f4ea59a42c40b6ef42bf99f106f43eb18d3ec452580784072046f099af26753e61e79704679fba7973bc19b59b3b9cdf0b537fd95e20caceeb18a719ef
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
# unreleased
|
2
|
+
|
3
|
+
* Introduce Mocktail.explain(), which will return a message & reference object
|
4
|
+
for any of:
|
5
|
+
* A class that has been passed to Mocktail.replace()
|
6
|
+
* An instance created by Mocktail.of() or of_next()
|
7
|
+
* A nil value returned by an unsatisfied stubbing invocation
|
8
|
+
* Fix some minor printing issue with the improved NoMethodError released in
|
9
|
+
0.0.3
|
10
|
+
|
11
|
+
|
1
12
|
# 0.0.3
|
2
13
|
|
3
14
|
* Implement method_missing on all mocked instance methods to print out useful
|
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,
|
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.map { |method| " - #{method}" }.join("\n")}
|
63
|
+
|
64
|
+
#{double.dry_methods.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
|
|
@@ -5,8 +5,7 @@ module Mocktail
|
|
5
5
|
@raises_neato_no_method_error = RaisesNeatoNoMethodError.new
|
6
6
|
end
|
7
7
|
|
8
|
-
def declare(type)
|
9
|
-
instance_methods = instance_methods_on(type)
|
8
|
+
def declare(type, instance_methods)
|
10
9
|
dry_class = Class.new(Object) {
|
11
10
|
include type if type.instance_of?(Module)
|
12
11
|
|
@@ -98,23 +97,5 @@ module Mocktail
|
|
98
97
|
)
|
99
98
|
}
|
100
99
|
end
|
101
|
-
|
102
|
-
def instance_methods_on(type)
|
103
|
-
methods = type.instance_methods + [
|
104
|
-
(:respond_to_missing? if type.private_method_defined?(:respond_to_missing?))
|
105
|
-
].compact
|
106
|
-
|
107
|
-
methods.reject { |m|
|
108
|
-
ignore?(type, m)
|
109
|
-
}
|
110
|
-
end
|
111
|
-
|
112
|
-
def ignore?(type, method_name)
|
113
|
-
ignored_ancestors.include?(type.instance_method(method_name).owner)
|
114
|
-
end
|
115
|
-
|
116
|
-
def ignored_ancestors
|
117
|
-
Object.ancestors
|
118
|
-
end
|
119
100
|
end
|
120
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
|
@@ -1,16 +1,18 @@
|
|
1
1
|
require_relative "share/stringifies_call"
|
2
|
+
require_relative "share/stringifies_method_name"
|
2
3
|
require_relative "share/creates_identifier"
|
3
4
|
|
4
5
|
module Mocktail
|
5
6
|
class RaisesNeatoNoMethodError
|
6
7
|
def initialize
|
7
8
|
@stringifies_call = StringifiesCall.new
|
9
|
+
@stringifies_method_name = StringifiesMethodName.new
|
8
10
|
@creates_identifier = CreatesIdentifier.new
|
9
11
|
end
|
10
12
|
|
11
13
|
def call(call)
|
12
14
|
raise NoMethodError.new <<~MSG
|
13
|
-
No method `#{
|
15
|
+
No method `#{@stringifies_method_name.stringify(call)}' exists for call:
|
14
16
|
|
15
17
|
#{@stringifies_call.stringify(call, anonymous_blocks: true, always_parens: true)}
|
16
18
|
|
@@ -1,13 +1,28 @@
|
|
1
1
|
module Mocktail
|
2
2
|
class CreatesIdentifier
|
3
|
+
KEYWORDS = %w[__FILE__ __LINE__ alias and begin BEGIN break case class def defined? do else elsif end END ensure false for if in module next nil not or redo rescue retry return self super then true undef unless until when while yield]
|
4
|
+
|
3
5
|
def create(s, default: "identifier", max_length: 24)
|
4
|
-
id = s.to_s.downcase
|
6
|
+
id = s.to_s.downcase
|
7
|
+
.gsub(/:0x[0-9a-f]+/, "") # Lazy attempt to wipe any Object:0x802beef identifiers
|
8
|
+
.gsub(/[^\w\s]/, "")
|
9
|
+
.gsub(/^\d+/, "")[0...max_length]
|
10
|
+
.strip
|
11
|
+
.gsub(/\s+/, "_") # snake_case
|
5
12
|
|
6
13
|
if id.empty?
|
7
14
|
default
|
8
15
|
else
|
9
|
-
id
|
16
|
+
unreserved(id, default)
|
10
17
|
end
|
11
18
|
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def unreserved(id, default)
|
23
|
+
return id unless KEYWORDS.include?(id)
|
24
|
+
|
25
|
+
"#{id}_#{default}"
|
26
|
+
end
|
12
27
|
end
|
13
28
|
end
|
@@ -37,5 +37,17 @@ module Mocktail
|
|
37
37
|
def demonstration_in_progress?
|
38
38
|
@demonstration_in_progress
|
39
39
|
end
|
40
|
+
|
41
|
+
def double_for_instance(thing)
|
42
|
+
@doubles.find { |double| double.dry_instance == thing }
|
43
|
+
end
|
44
|
+
|
45
|
+
def stubbings_for_double(double)
|
46
|
+
@stubbings.select { |stubbing| stubbing.recording.double == double.dry_instance }
|
47
|
+
end
|
48
|
+
|
49
|
+
def calls_for_double(double)
|
50
|
+
@calls.select { |call| call.double == double.dry_instance }
|
51
|
+
end
|
40
52
|
end
|
41
53
|
end
|
@@ -1,11 +1,10 @@
|
|
1
1
|
module Mocktail
|
2
|
-
class Double
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
2
|
+
class Double < Struct.new(
|
3
|
+
:original_type,
|
4
|
+
:dry_type,
|
5
|
+
:dry_instance,
|
6
|
+
:dry_methods,
|
7
|
+
keyword_init: true
|
8
|
+
)
|
10
9
|
end
|
11
10
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Mocktail
|
2
|
+
class Explanation
|
3
|
+
attr_reader :reference, :message
|
4
|
+
|
5
|
+
def initialize(reference, message)
|
6
|
+
@reference = reference
|
7
|
+
@message = message
|
8
|
+
end
|
9
|
+
|
10
|
+
def type
|
11
|
+
self.class
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class NoExplanation < Explanation
|
16
|
+
end
|
17
|
+
|
18
|
+
class UnsatisfiedStubExplanation < Explanation
|
19
|
+
end
|
20
|
+
|
21
|
+
class DoubleExplanation < Explanation
|
22
|
+
end
|
23
|
+
|
24
|
+
class ReplacedTypeExplanation < Explanation
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
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
|
@@ -17,6 +17,10 @@ module Mocktail
|
|
17
17
|
@type_replacements[type] ||= TypeReplacement.new(type: type)
|
18
18
|
end
|
19
19
|
|
20
|
+
def type_replacement_if_exists_for(type)
|
21
|
+
@type_replacements[type]
|
22
|
+
end
|
23
|
+
|
20
24
|
def reset_current_thread!
|
21
25
|
@new_registrations[Thread.current] = []
|
22
26
|
@of_next_registrations[Thread.current] = []
|
data/lib/mocktail/value.rb
CHANGED
@@ -2,8 +2,13 @@ require_relative "value/cabinet"
|
|
2
2
|
require_relative "value/call"
|
3
3
|
require_relative "value/demo_config"
|
4
4
|
require_relative "value/double"
|
5
|
+
require_relative "value/double_data"
|
6
|
+
require_relative "value/explanation"
|
5
7
|
require_relative "value/matcher_registry"
|
6
8
|
require_relative "value/signature"
|
7
9
|
require_relative "value/stubbing"
|
10
|
+
require_relative "value/stub_returned_nil"
|
8
11
|
require_relative "value/top_shelf"
|
9
12
|
require_relative "value/type_replacement"
|
13
|
+
require_relative "value/type_replacement_data"
|
14
|
+
require_relative "value/unsatisfied_stubbing"
|
@@ -1,16 +1,18 @@
|
|
1
1
|
require_relative "raises_verification_error/gathers_calls_of_method"
|
2
|
+
require_relative "../share/stringifies_method_name"
|
2
3
|
require_relative "../share/stringifies_call"
|
3
4
|
|
4
5
|
module Mocktail
|
5
6
|
class RaisesVerificationError
|
6
7
|
def initialize
|
7
8
|
@gathers_calls_of_method = GathersCallsOfMethod.new
|
9
|
+
@stringifies_method_name = StringifiesMethodName.new
|
8
10
|
@stringifies_call = StringifiesCall.new
|
9
11
|
end
|
10
12
|
|
11
13
|
def raise(recording, verifiable_calls, demo_config)
|
12
14
|
Kernel.raise VerificationError.new <<~MSG
|
13
|
-
Expected mocktail of
|
15
|
+
Expected mocktail of `#{@stringifies_method_name.stringify(recording)}' to be called like:
|
14
16
|
|
15
17
|
#{@stringifies_call.stringify(recording)}#{[
|
16
18
|
(" [#{demo_config.times} #{pl("time", demo_config.times)}]" unless demo_config.times.nil?),
|
data/lib/mocktail/version.rb
CHANGED
data/lib/mocktail.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require_relative "mocktail/dsl"
|
2
2
|
require_relative "mocktail/errors"
|
3
|
+
require_relative "mocktail/explains_thing"
|
3
4
|
require_relative "mocktail/handles_dry_call"
|
4
5
|
require_relative "mocktail/handles_dry_new_call"
|
5
6
|
require_relative "mocktail/imitates_type"
|
@@ -56,7 +57,13 @@ module Mocktail
|
|
56
57
|
ResetsState.new.reset
|
57
58
|
end
|
58
59
|
|
60
|
+
def self.explain(thing)
|
61
|
+
ExplainsThing.new.explain(thing)
|
62
|
+
end
|
63
|
+
|
59
64
|
# Stores most transactional state about calls & stubbing configurations
|
65
|
+
# Anything returned by this is undocumented and could change at any time, so
|
66
|
+
# don't commit code that relies on it!
|
60
67
|
def self.cabinet
|
61
68
|
Thread.current[:mocktail_store] ||= Cabinet.new
|
62
69
|
end
|
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: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Searls
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-10-
|
11
|
+
date: 2021-10-07 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -31,8 +31,10 @@ files:
|
|
31
31
|
- lib/mocktail.rb
|
32
32
|
- lib/mocktail/dsl.rb
|
33
33
|
- lib/mocktail/errors.rb
|
34
|
+
- lib/mocktail/explains_thing.rb
|
34
35
|
- lib/mocktail/handles_dry_call.rb
|
35
36
|
- lib/mocktail/handles_dry_call/fulfills_stubbing.rb
|
37
|
+
- lib/mocktail/handles_dry_call/fulfills_stubbing/describes_unsatisfied_stubbing.rb
|
36
38
|
- lib/mocktail/handles_dry_call/fulfills_stubbing/finds_satisfaction.rb
|
37
39
|
- lib/mocktail/handles_dry_call/logs_call.rb
|
38
40
|
- lib/mocktail/handles_dry_call/validates_arguments.rb
|
@@ -41,6 +43,7 @@ files:
|
|
41
43
|
- lib/mocktail/imitates_type/ensures_imitation_support.rb
|
42
44
|
- lib/mocktail/imitates_type/makes_double.rb
|
43
45
|
- lib/mocktail/imitates_type/makes_double/declares_dry_class.rb
|
46
|
+
- lib/mocktail/imitates_type/makes_double/gathers_fakeable_instance_methods.rb
|
44
47
|
- lib/mocktail/initializes_mocktail.rb
|
45
48
|
- lib/mocktail/matcher_presentation.rb
|
46
49
|
- lib/mocktail/matchers.rb
|
@@ -65,6 +68,7 @@ files:
|
|
65
68
|
- lib/mocktail/share/creates_identifier.rb
|
66
69
|
- lib/mocktail/share/determines_matching_calls.rb
|
67
70
|
- lib/mocktail/share/stringifies_call.rb
|
71
|
+
- lib/mocktail/share/stringifies_method_name.rb
|
68
72
|
- lib/mocktail/simulates_argument_error.rb
|
69
73
|
- lib/mocktail/simulates_argument_error/cleans_backtrace.rb
|
70
74
|
- lib/mocktail/simulates_argument_error/reconciles_args_with_params.rb
|
@@ -75,11 +79,16 @@ files:
|
|
75
79
|
- lib/mocktail/value/call.rb
|
76
80
|
- lib/mocktail/value/demo_config.rb
|
77
81
|
- lib/mocktail/value/double.rb
|
82
|
+
- lib/mocktail/value/double_data.rb
|
83
|
+
- lib/mocktail/value/explanation.rb
|
78
84
|
- lib/mocktail/value/matcher_registry.rb
|
79
85
|
- lib/mocktail/value/signature.rb
|
86
|
+
- lib/mocktail/value/stub_returned_nil.rb
|
80
87
|
- lib/mocktail/value/stubbing.rb
|
81
88
|
- lib/mocktail/value/top_shelf.rb
|
82
89
|
- lib/mocktail/value/type_replacement.rb
|
90
|
+
- lib/mocktail/value/type_replacement_data.rb
|
91
|
+
- lib/mocktail/value/unsatisfied_stubbing.rb
|
83
92
|
- lib/mocktail/verifies_call.rb
|
84
93
|
- lib/mocktail/verifies_call/finds_verifiable_calls.rb
|
85
94
|
- lib/mocktail/verifies_call/raises_verification_error.rb
|