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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e719e2144c3da8a2569e2ef5e7cf2c78d054625011fcf0bdf8f98f300bdcd805
4
- data.tar.gz: 3635b7210ac6d5a7c05c4000f100ac1e07f4c8d5de7de81586e41f3b81e11ce5
3
+ metadata.gz: 426f5dbb08f6db2542f0a323d186df7126aaae652e848b0a0c378a602231d777
4
+ data.tar.gz: 898501920257accc975d441a12d9e54a034c40aef8c950be2f0fa80aeb0288c6
5
5
  SHA512:
6
- metadata.gz: d84b7964c5fbe14e3ef2a8a881c1c328f0f4c37cbde2d1423965016fe26f153c6801032c5aa4a7018db6d98924aecbb031e3e473992178bb9d5fe46e53ac2ea5
7
- data.tar.gz: 384e42b818b92ace165e975dafd1482e00a46fc3be4c4abf0bb54e2ff648f29da1b338a8845be5c64e1878abf738e4061345083c0e952206380119ecb82729cf
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mocktail (0.0.3)
4
+ mocktail (0.0.4)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -50,26 +50,37 @@ assert_equal "🎉", result
50
50
  verify { glass.pour!(:a_drink) }
51
51
  ```
52
52
 
53
- ## Why order?
54
-
55
- Besides a lack of hangover, Mocktail offers several advantages over other
56
- mocking libraries:
57
-
58
- * **Fewer hoops to jump through**: [`Mocktail.of_next(type)`] avoids the need
59
- for dependency injection by returning a Mocktail of the type the next time
60
- `Type.new` is called. You can inject a fake into production code in one
61
- line.
62
- * **Fewer false test passes**: Arity of arguments and keyword arguments of faked
63
- methods is enforced—no more tests that keep passing after an API changes
64
- * **Super-duper detailed error messages when verifications fail**
65
- * **Fake class methods**: Singleton methods on classes and modules can be
66
- replaced with [`Mocktail.replace(type)`](#mocktailreplace) while still
67
- preserving thread safety
68
- * **Less test setup**: Dynamic stubbings based on the arguments passed to the actual call
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
- # (If you use this, don't forget to add pry to your Gemfile!)
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(klass)
10
- dry_type = @declares_dry_class.declare(klass)
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: klass,
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 `#{call.original_type.name}##{call.method}' exists for call:
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.gsub(/[^\w\s]/, "").gsub(/^\d+/, "")[0...max_length].strip.gsub(/\s+/, "_")
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
@@ -0,0 +1,11 @@
1
+ module Mocktail
2
+ class StringifiesMethodName
3
+ def stringify(call)
4
+ [
5
+ call.original_type.name,
6
+ call.singleton ? "." : "#",
7
+ call.method
8
+ ].join
9
+ end
10
+ end
11
+ 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
- attr_reader :original_type, :dry_type, :dry_instance
4
-
5
- def initialize(original_type:, dry_type:, dry_instance:)
6
- @original_type = original_type
7
- @dry_type = dry_type
8
- @dry_instance = dry_instance
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,10 @@
1
+ module Mocktail
2
+ class DoubleData < Struct.new(
3
+ :type,
4
+ :double,
5
+ :calls,
6
+ :stubbings,
7
+ keyword_init: true
8
+ )
9
+ end
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] = []
@@ -0,0 +1,13 @@
1
+ module Mocktail
2
+ class TypeReplacementData < Struct.new(
3
+ :type,
4
+ :replaced_method_names,
5
+ :calls,
6
+ :stubbings,
7
+ keyword_init: true
8
+ )
9
+ def double
10
+ type
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ module Mocktail
2
+ class UnsatisfiedStubbing < Struct.new(
3
+ :call,
4
+ :other_stubbings,
5
+ keyword_init: true
6
+ )
7
+ end
8
+ end
@@ -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 #{recording.original_type.name}##{recording.method} to be called like:
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?),
@@ -1,3 +1,3 @@
1
1
  module Mocktail
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
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.3
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-04 00:00:00.000000000 Z
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