mocktail 0.0.2 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/Gemfile.lock +2 -2
  4. data/README.md +178 -18
  5. data/bin/console +21 -1
  6. data/lib/mocktail/explains_thing.rb +132 -0
  7. data/lib/mocktail/handles_dry_call/fulfills_stubbing/describes_unsatisfied_stubbing.rb +13 -0
  8. data/lib/mocktail/handles_dry_call/fulfills_stubbing.rb +4 -0
  9. data/lib/mocktail/handles_dry_call/validates_arguments.rb +2 -23
  10. data/lib/mocktail/imitates_type/makes_double/declares_dry_class.rb +29 -23
  11. data/lib/mocktail/imitates_type/makes_double/gathers_fakeable_instance_methods.rb +21 -0
  12. data/lib/mocktail/imitates_type/makes_double.rb +8 -4
  13. data/lib/mocktail/raises_neato_no_method_error.rb +81 -0
  14. data/lib/mocktail/share/creates_identifier.rb +28 -0
  15. data/lib/mocktail/{verifies_call/raises_verification_error → share}/stringifies_call.rb +16 -7
  16. data/lib/mocktail/share/stringifies_method_name.rb +11 -0
  17. data/lib/mocktail/simulates_argument_error/cleans_backtrace.rb +17 -0
  18. data/lib/mocktail/simulates_argument_error/reconciles_args_with_params.rb +20 -0
  19. data/lib/mocktail/simulates_argument_error/recreates_message.rb +29 -0
  20. data/lib/mocktail/simulates_argument_error/transforms_params.rb +32 -0
  21. data/lib/mocktail/simulates_argument_error.rb +30 -0
  22. data/lib/mocktail/value/cabinet.rb +12 -0
  23. data/lib/mocktail/value/double.rb +7 -8
  24. data/lib/mocktail/value/double_data.rb +10 -0
  25. data/lib/mocktail/value/explanation.rb +26 -0
  26. data/lib/mocktail/value/signature.rb +36 -0
  27. data/lib/mocktail/value/stub_returned_nil.rb +26 -0
  28. data/lib/mocktail/value/top_shelf.rb +24 -25
  29. data/lib/mocktail/value/type_replacement_data.rb +13 -0
  30. data/lib/mocktail/value/unsatisfied_stubbing.rb +8 -0
  31. data/lib/mocktail/value.rb +6 -0
  32. data/lib/mocktail/verifies_call/raises_verification_error.rb +4 -2
  33. data/lib/mocktail/version.rb +1 -1
  34. data/lib/mocktail.rb +9 -0
  35. data/mocktail.gemspec +1 -1
  36. metadata +22 -6
  37. 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: d5a4729eb601d9f3ed3ece213a460dd0881105a505a812c4d035c14cec252223
4
- data.tar.gz: f4b3a3352f63de7de812ce8db616b570de740f1ce7550f8677eb47ecb99305fb
3
+ metadata.gz: 67768c396af3e99d7ef7649d2afecf023f1dbdc0e361d960bebbf6befe908d3b
4
+ data.tar.gz: 196930b31afa82713bf68469c6d3cb434ce67f22f6cc4c7393c7c438201550c9
5
5
  SHA512:
6
- metadata.gz: 469b39914d0d887b7cd8d25006f5dc6f3ca8a1a3b5594f39bfae5e3c21f36f2983a30ec2849c916129e216dd9fb731f476abbdc5a41ad0f80c740d7106793a7d
7
- data.tar.gz: d7c24487a15c7ddc9b540d6c9d5c2b3f07da3599eba61eee128a79f5faa3fd5f6c4e54b7ea4318119a86737ead855f30d814a7f39e30c1e20a6f3880f0c21ce2
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mocktail (0.0.2)
4
+ mocktail (0.0.6)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -60,4 +60,4 @@ DEPENDENCIES
60
60
  standard
61
61
 
62
62
  BUNDLED WITH
63
- 2.2.15
63
+ 2.2.30
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,
@@ -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
- # (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.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
- arg_params, kwarg_params = dry_call.original_method.parameters.reject { |type, _|
34
- type == :block
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 type_type == :module
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 type_type == :class
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
- add_stringify_methods!(dry_class, :to_s, type, type_type, instance_methods)
29
- add_stringify_methods!(dry_class, :inspect, type, type_type, instance_methods)
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: self.class,
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, type_type, instance_methods)
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 type_of(type)
78
- if type.is_a?(Class)
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
- def instance_methods_on(type)
86
- type.instance_methods.reject { |m|
87
- ignored_ancestors.include?(type.instance_method(m).owner)
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(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