mocktail 1.0.0 → 1.1.2

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: 05ec28e8027b6d73ee5b3f513ce4d5e23a60ea0266643e344e20e8f995410ce2
4
- data.tar.gz: 7baf1cff682de031f3f25cebad265e59e598c4f4103968ea2dda61b6c7605c9c
3
+ metadata.gz: 7eb64f90875e53a9bfd5b4d995f8526eb6695dd9bd8a88c850b02eed851cb96d
4
+ data.tar.gz: ae2b4443a3740c375c70e7e490cbe3463d9672f17665ae47930b6ee614e9891d
5
5
  SHA512:
6
- metadata.gz: 4c5eef4fe5c68abd7d1e506bc59db58daa931fb484118e0ee4d6d286944d2834d0bb3107183102ac65110e12f32992b73fadbfcc26ed750bff060535e4eba5a2
7
- data.tar.gz: cff11562ac6bc79475719f17f912ffc2fe8af6315ab90dbdc838143c237542ad788cba2d6c05ee15417799b2f597634244edf3c9f50dac5121adbac551602bdb
6
+ metadata.gz: de8d47071e93c4d406391a1155dea9b6a1a9ac89f56422ae29e6dda5f134c9e5e724ec8006aba833b5ff19d9d2dff4550384bc349506f5ab9cefe3fd2185af1d
7
+ data.tar.gz: 5bf88d0aadc08fe9a9380312cf491146439e72e335091e2e79849196b74e93aba20b0fac2d122152a4661008ee243b5b3b39cb6408fc776b2cc2567ffca0924f
@@ -7,7 +7,7 @@ jobs:
7
7
  strategy:
8
8
  matrix:
9
9
  os: [ ubuntu-latest ]
10
- ruby-version: [3.0.1]
10
+ ruby-version: ['3.0', '3.1']
11
11
 
12
12
  runs-on: ${{ matrix.os }}
13
13
 
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ # 1.1.1
2
+
3
+ * Improve output for undefined singleton methods
4
+ ([#11](https://github.com/testdouble/mocktail/pull/11) by
5
+ [@calebhearth](https://github.com/calebhearth))
6
+
7
+ # 1.1.0
8
+
9
+ * Feature: add support for passing methods to `Mocktail.explain()`
10
+ * Fix 3.1 support by bypassing highlight_error for custom NoMethodError objects
11
+ raised by Mocktail [error_highlight#20](https://github.com/ruby/error_highlight/issues/20)
12
+
1
13
  # 1.0.0
2
14
 
3
15
  * First breaking change! 🎉
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mocktail (1.0.0)
4
+ mocktail (1.1.2)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -11,28 +11,28 @@ GEM
11
11
  docile (1.4.0)
12
12
  method_source (1.0.0)
13
13
  minitest (5.15.0)
14
- parallel (1.21.0)
15
- parser (3.0.3.2)
14
+ parallel (1.22.1)
15
+ parser (3.1.2.0)
16
16
  ast (~> 2.4.1)
17
17
  pry (0.14.1)
18
18
  coderay (~> 1.1)
19
19
  method_source (~> 1.0)
20
- rainbow (3.0.0)
20
+ rainbow (3.1.1)
21
21
  rake (13.0.6)
22
- regexp_parser (2.2.0)
22
+ regexp_parser (2.3.0)
23
23
  rexml (3.2.5)
24
- rubocop (1.23.0)
24
+ rubocop (1.27.0)
25
25
  parallel (~> 1.10)
26
- parser (>= 3.0.0.0)
26
+ parser (>= 3.1.0.0)
27
27
  rainbow (>= 2.2.2, < 4.0)
28
28
  regexp_parser (>= 1.8, < 3.0)
29
29
  rexml
30
- rubocop-ast (>= 1.12.0, < 2.0)
30
+ rubocop-ast (>= 1.16.0, < 2.0)
31
31
  ruby-progressbar (~> 1.7)
32
32
  unicode-display_width (>= 1.4.0, < 3.0)
33
- rubocop-ast (1.15.0)
34
- parser (>= 3.0.1.1)
35
- rubocop-performance (1.12.0)
33
+ rubocop-ast (1.17.0)
34
+ parser (>= 3.1.1.0)
35
+ rubocop-performance (1.13.3)
36
36
  rubocop (>= 1.7.0, < 2.0)
37
37
  rubocop-ast (>= 0.4.0)
38
38
  ruby-progressbar (1.11.0)
@@ -41,10 +41,10 @@ GEM
41
41
  simplecov-html (~> 0.11)
42
42
  simplecov_json_formatter (~> 0.1)
43
43
  simplecov-html (0.12.3)
44
- simplecov_json_formatter (0.1.3)
45
- standard (1.5.0)
46
- rubocop (= 1.23.0)
47
- rubocop-performance (= 1.12.0)
44
+ simplecov_json_formatter (0.1.4)
45
+ standard (1.10.0)
46
+ rubocop (= 1.27.0)
47
+ rubocop-performance (= 1.13.3)
48
48
  unicode-display_width (2.1.0)
49
49
 
50
50
  PLATFORMS
@@ -60,4 +60,4 @@ DEPENDENCIES
60
60
  standard
61
61
 
62
62
  BUNDLED WITH
63
- 2.2.30
63
+ 2.3.6
data/README.md CHANGED
@@ -10,6 +10,11 @@ library for Ruby that provides a terse and robust API for creating mocks,
10
10
  getting them in the hands of the code you're testing, stub & verify behavior,
11
11
  and even safely override class methods.
12
12
 
13
+ If you'd prefer a voice & video introduction to Mocktail aside from this README,
14
+ you might enjoy this ⚡️[Lightning
15
+ Talk](https://blog.testdouble.com/talks/2022-05-18-please-mock-me?utm_source=twitter&utm_medium=organic-social&utm_campaign=conf-talk)⚡️
16
+ from RailsConf 2022.
17
+
13
18
  ## An aperitif
14
19
 
15
20
  Before getting into the details, let's demonstrate what Mocktail's API looks
@@ -553,14 +558,160 @@ them!)]
553
558
  ### Mocktail.explain
554
559
 
555
560
  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.
561
+ which way, so Mocktail tries to make it a little easier on you. In addition to
562
+ returning useful messages throughout the API, the gem also includes an
563
+ introspection method `Mocktail.explain(thing)`, which returns a human-readable
564
+ `message` and a `reference` object with useful attributes (that vary depending
565
+ on the type of fake `thing` you pass in. Below are some things `explain()` can
566
+ do.
567
+
568
+ #### Fake instances created by Mocktail
569
+
570
+ Any instances created by `Mocktail.of` or `Mocktail.of_next` can be passed to
571
+ `Mocktail.explain`, and they will list out all the calls and stubbings made for
572
+ each of their fake methods.
573
+
574
+ Suppose these interactions have occurred:
575
+
576
+ ```ruby
577
+ ice_tray = Mocktail.of(IceTray)
578
+
579
+ Mocktail.stubs { ice_tray.fill(:tap_water, 30) }.with { :some_ice }
580
+
581
+ ice_tray.fill(:tap_water, 50)
582
+ ```
583
+
584
+ You can interrogate what's going on with the fake instance by passing it to
585
+ `explain`:
586
+
587
+ ```ruby
588
+ explanation = Mocktail.explain(ice_tray)
589
+
590
+ explanation.reference.type #=> IceTray
591
+ explanation.reference.double #=> The ice_tray instance
592
+ explanation.reference.calls #=> details on each invocation of each method
593
+ explanation.reference.stubbings #=> all stubbings configured for each method
594
+ ```
595
+
596
+ Calling `explanation.message` will return:
597
+
598
+ ```
599
+ This is a fake `IceTray' instance.
600
+
601
+ It has these mocked methods:
602
+ - fill
603
+
604
+ `IceTray#fill' stubbings:
605
+
606
+ fill(:tap_water, 30)
607
+
608
+ `IceTray#fill' calls:
609
+
610
+ fill(:tap_water, 50)
611
+
612
+ ```
613
+
614
+ #### Modules and classes with singleton methods replaced
615
+
616
+ If you've called `Mocktail.replace()` on a class or module, it can also be
617
+ passed to `Mocktail.explain()` for a summary of all the stubbing configurations
618
+ and calls made against its faked singleton methods for the currently running
619
+ thread.
620
+
621
+ Imagine a `Shop` class with `self.open!` and `self.close!` singleton methods:
622
+
623
+ ```ruby
624
+ Mocktail.replace(Shop)
625
+
626
+ stubs { |m| Shop.open!(m.numeric) }.with { :a_bar }
627
+
628
+ Shop.open!(42)
629
+
630
+ Shop.close!(42)
631
+
632
+ explanation = Mocktail.explain(Shop)
633
+
634
+ explanation.reference.type #=> Shop
635
+ explanation.reference.replaced_method_names #=> [:close!, :open!]
636
+ explanation.reference.calls #=> details on each invocation of each method
637
+ explanation.reference.stubbings #=> all stubbings configured for each method
638
+ ```
639
+
640
+ And `explanation.message` will return:
641
+
642
+ ```ruby
643
+ `Shop' is a class that has had its singleton methods faked.
644
+
645
+ It has these mocked methods:
646
+ - close!
647
+ - open!
648
+
649
+ `Shop.close!' has no stubbings.
650
+
651
+ `Shop.close!' calls:
652
+
653
+ close!(42)
654
+
655
+ close!(42)
656
+
657
+ `Shop.open!' stubbings:
658
+
659
+ open!(numeric)
660
+
661
+ open!(numeric)
662
+
663
+ `Shop.open!' calls:
664
+
665
+ open!(42)
666
+
667
+ open!(42)
668
+ ```
669
+
670
+ #### Methods on faked instances and replaced types
671
+
672
+ In addition to passing the test double, you can also pass a reference to any
673
+ fake method created by Mocktail to `Mocktail.explain`:
674
+
675
+ ```ruby
676
+ ice_tray = Mocktail.of(IceTray)
677
+
678
+ ice_tray.fill(:chilled, 50)
679
+
680
+ explanation = Mocktail.explain(ice_tray.method(:fill))
681
+
682
+ explanation.reference.receiver #=> a reference to the `ice_tray` instance
683
+ explanation.reference.calls #=> details on each invocation of the method
684
+ explanation.reference.stubbings #=> all stubbings configured for the method
685
+ ```
686
+
687
+ The above may be handy in cases where you want to assert the number of calls of
688
+ a method outside the `Mocktail.verify` API:
689
+
690
+ ```ruby
691
+ assert_equal 1, explanation.reference.calls.size
692
+ ```
693
+
694
+ The explanation will also contain a `message` like this:
695
+
696
+ ```
697
+ `IceTray#fill' has no stubbings.
698
+
699
+ `IceTray#fill' calls:
700
+
701
+ fill(:chilled, 50)
702
+ ```
703
+
704
+ Replaced singleton methods can also be passed to `explain()`, so something like
705
+ `Mocktail.explain(Shop.method(:open!))` from the earlier example would also work
706
+ (with `Shop` being the `receiver` on the explanation's `reference`).
558
707
 
559
708
  #### Undefined methods
560
709
 
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:
710
+ There's no API for this one, but Mocktail also offers explanations for methods
711
+ that don't exist yet. You'll see this error message whenever you try to call a
712
+ method that doesn't exist on a test double. The message is designed to
713
+ facilitate "paint-by-numbers" TDD, by including a sample definition of the
714
+ method you had attempted to call that can be copy-pasted into a source listing:
564
715
 
565
716
  ```ruby
566
717
  class IceTray
@@ -589,7 +740,7 @@ class IceTray
589
740
  end
590
741
  ```
591
742
 
592
- ### Unexpected nils with Mocktail.explain_nils
743
+ ### Mocktail.explain_nils
593
744
 
594
745
  Is a faked method returning `nil` and you don't understand why?
595
746
 
@@ -656,75 +807,6 @@ The `reference` object will have details of the `call` itself, an array of
656
807
  `other_stubbings` defined on the faked method, and a `backtrace` to determine
657
808
  which call site produced the unexpected `nil` value.
658
809
 
659
- #### Fake instances created by Mocktail
660
-
661
- Any instances created by `Mocktail.of` or `Mocktail.of_next` can be passed to
662
- `Mocktail.explain`, and they will list out all the calls and stubbings made for
663
- each of their fake methods.
664
-
665
- Calling `Mocktail.explain(ice_tray).message` following the example above will
666
- yield:
667
-
668
- ```
669
- This is a fake `IceTray' instance.
670
-
671
- It has these mocked methods:
672
- - fill
673
-
674
- `IceTray#fill' stubbings:
675
-
676
- fill(:tap_water, 30)
677
-
678
- `IceTray#fill' calls:
679
-
680
- fill(:tap_water, 50)
681
- ```
682
-
683
- #### Modules and classes with singleton methods replaced
684
-
685
- If you've called `Mocktail.replace()` on a class or module, it can also be
686
- passed to `Mocktail.explain()` for a summary of all the stubbing configurations
687
- and calls made against its faked singleton methods for the currently running
688
- thread.
689
-
690
- Imagine a `Shop` class with `self.open!` and `self.close!` singleton methods:
691
-
692
- ```ruby
693
- Mocktail.replace(Shop)
694
-
695
- stubs { |m| Shop.open!(m.numeric) }.with { :a_bar }
696
-
697
- Shop.open!(42)
698
-
699
- Shop.close!(42)
700
-
701
- puts Mocktail.explain(Shop).message
702
- ```
703
-
704
- Will print:
705
-
706
- ```ruby
707
- `Shop' is a class that has had its singleton methods faked.
708
-
709
- It has these mocked methods:
710
- - close!
711
- - open!
712
-
713
- `Shop.close!' has no stubbings.
714
-
715
- `Shop.close!' calls:
716
-
717
- close!(42)
718
-
719
- `Shop.open!' stubbings:
720
-
721
- open!(numeric)
722
-
723
- `Shop.open!' calls:
724
-
725
- open!(42)
726
- ```
727
-
728
810
  ### Mocktail.reset
729
811
 
730
812
  This one's simple: you probably want to call `Mocktail.reset` after each test,
@@ -736,10 +818,35 @@ Calling reset in a `teardown` or `after(:each)` hook will also improve the
736
818
  usefulness of messages returned by `Mocktail.explain` and
737
819
  `Mocktail.explain_nils`.
738
820
 
821
+ ## References
822
+
823
+ Mocktail is designed following a somewhat academic understanding of what mocking
824
+ is and how it should be used. Below are several references on this topic.
825
+
826
+ Blog Posts and Papers:
827
+
828
+ - [Endo-Testing: Unit Testing with Mock
829
+ Objects](<https://www2.ccs.neu.edu/research/demeter/related-work/extreme-programming/MockObjectsFinal.PDF>
830
+ by Tim Mackinnon, Steve Freeman, and Philip Craig, the paper that introduced
831
+ mocking presented by the creators of mocking.
832
+ - Michael Feathers' [The Flawed Theory Behind Unit
833
+ Testing](<https://michaelfeathers.typepad.com/michael_feathers_blog/2008/06/the-flawed-theo.html>)
834
+
835
+ Books:
836
+
837
+ - [_Growing Object-Oriented Software, Guided by
838
+ Tests_](<https://bookshop.org/books/growing-object-oriented-software-guided-by-tests/9780321503626>)
839
+ by Steve Freeman and Nat Price
840
+
841
+ Talks:
842
+
843
+ - [Please don’t mock me](https://www.youtube.com/watch?v=Af4M8GMoxi4) by Justin
844
+ Searls
845
+
739
846
  ## Acknowledgements
740
847
 
741
848
  Mocktail is created & maintained by the software agency [Test
742
- Double](https://twitter.com). If you've ever come across our eponymously-named
849
+ Double](https://testdouble.com). If you've ever come across our eponymously-named
743
850
  [testdouble.js](https://github.com/testdouble/testdouble.js/), you might find
744
851
  Mocktail's API to be quite similar. The term "test double" was originally coined
745
852
  by Gerard Meszaros in his book [xUnit Test
@@ -762,4 +869,3 @@ including (but not limited to) one-on-one communications, public posts/comments,
762
869
  code reviews, pull requests, and GitHub issues. If violations occur, Test Double
763
870
  will take any action they deem appropriate for the infraction, up to and
764
871
  including blocking a user from the organization's repositories.
765
-
@@ -0,0 +1,49 @@
1
+ module Mocktail
2
+ module Debug
3
+ # It would be easy and bad for the mocktail lib to call something like
4
+ #
5
+ # double == other_double
6
+ #
7
+ # But if it's a double, that means anyone who stubs that method could change
8
+ # the internal behavior of the library in unexpected ways (as happened here:
9
+ # https://github.com/testdouble/mocktail/issues/7 )
10
+ #
11
+ # For that reason when we run our tests, we also want to blow up if this
12
+ # happens unintentionally. This works in conjunction with the test
13
+ # MockingMethodfulClassesTest, because it mocks every defined method on the
14
+ # mocked BasicObject
15
+ def self.guard_against_mocktail_accidentally_calling_mocks_if_debugging!
16
+ return unless ENV["MOCKTAIL_DEBUG_ACCIDENTAL_INTERNAL_MOCK_CALLS"]
17
+ raise
18
+ rescue => e
19
+ base_path = Pathname.new(__FILE__).dirname.to_s
20
+ backtrace_minus_this_and_whoever_called_this = e.backtrace[2..]
21
+ internal_call_sites = backtrace_minus_this_and_whoever_called_this.take_while { |call_site|
22
+ # the "in `block" is very confusing but necessary to include lines after
23
+ # a stubs { blah.foo }.with { … } call, since that's when most of the
24
+ # good stuff happens
25
+ call_site.start_with?(base_path) || call_site.include?("in `block")
26
+ }.reject { |call_site| call_site.include?("in `block") }
27
+
28
+ approved_call_sites = [
29
+ "fulfills_stubbing.rb:14",
30
+ "validates_arguments.rb:16",
31
+ "validates_arguments.rb:19"
32
+ ]
33
+ if internal_call_sites.any? && approved_call_sites.none? { |approved_call_site|
34
+ internal_call_sites.first.include?(approved_call_site)
35
+ }
36
+ raise Error.new <<~MSG
37
+ Unauthorized internal call of a mock internally by Mocktail itself:
38
+
39
+ #{internal_call_sites.first}
40
+
41
+ Offending call's complete stack trace:
42
+
43
+ #{backtrace_minus_this_and_whoever_called_this.join("\n")}
44
+ ==END OFFENDING TRACE==
45
+ MSG
46
+ end
47
+ end
48
+ end
49
+ end
@@ -13,6 +13,8 @@ module Mocktail
13
13
  double_explanation(double)
14
14
  elsif (type_replacement = TopShelf.instance.type_replacement_if_exists_for(thing))
15
15
  replaced_type_explanation(type_replacement)
16
+ elsif (fake_method_explanation = fake_method_explanation_for(thing))
17
+ fake_method_explanation
16
18
  else
17
19
  no_explanation(thing)
18
20
  end
@@ -20,13 +22,37 @@ module Mocktail
20
22
 
21
23
  private
22
24
 
23
- def double_explanation(double)
24
- double_data = DoubleData.new(
25
+ def fake_method_explanation_for(thing)
26
+ return unless thing.is_a?(Method)
27
+ method = thing
28
+ receiver = thing.receiver
29
+
30
+ receiver_data = if (double = Mocktail.cabinet.double_for_instance(receiver))
31
+ data_for_double(double)
32
+ elsif (type_replacement = TopShelf.instance.type_replacement_if_exists_for(receiver))
33
+ data_for_type_replacement(type_replacement)
34
+ end
35
+
36
+ if receiver_data
37
+ FakeMethodExplanation.new(FakeMethodData.new(
38
+ receiver: receiver,
39
+ calls: receiver_data.calls,
40
+ stubbings: receiver_data.stubbings
41
+ ), describe_dry_method(receiver_data, method.name))
42
+ end
43
+ end
44
+
45
+ def data_for_double(double)
46
+ DoubleData.new(
25
47
  type: double.original_type,
26
48
  double: double.dry_instance,
27
49
  calls: Mocktail.cabinet.calls_for_double(double),
28
50
  stubbings: Mocktail.cabinet.stubbings_for_double(double)
29
51
  )
52
+ end
53
+
54
+ def double_explanation(double)
55
+ double_data = data_for_double(double)
30
56
 
31
57
  DoubleExplanation.new(double_data, <<~MSG)
32
58
  This is a fake `#{double.original_type.name}' instance.
@@ -38,8 +64,8 @@ module Mocktail
38
64
  MSG
39
65
  end
40
66
 
41
- def replaced_type_explanation(type_replacement)
42
- type_replacement_data = TypeReplacementData.new(
67
+ def data_for_type_replacement(type_replacement)
68
+ TypeReplacementData.new(
43
69
  type: type_replacement.type,
44
70
  replaced_method_names: type_replacement.replacement_methods.map(&:name).sort,
45
71
  calls: Mocktail.cabinet.calls.select { |call|
@@ -49,6 +75,10 @@ module Mocktail
49
75
  stubbing.recording.double == type_replacement.type
50
76
  }
51
77
  )
78
+ end
79
+
80
+ def replaced_type_explanation(type_replacement)
81
+ type_replacement_data = data_for_type_replacement(type_replacement)
52
82
 
53
83
  ReplacedTypeExplanation.new(type_replacement_data, <<~MSG)
54
84
  `#{type_replacement.type}' is a #{type_replacement.type.class.to_s.downcase} that has had its singleton methods faked.
@@ -1,17 +1,19 @@
1
1
  require_relative "../../share/cleans_backtrace"
2
+ require_relative "../../share/compares_safely"
2
3
 
3
4
  module Mocktail
4
5
  class DescribesUnsatisfiedStubbing
5
6
  def initialize
6
7
  @cleans_backtrace = CleansBacktrace.new
8
+ @compares_safely = ComparesSafely.new
7
9
  end
8
10
 
9
11
  def describe(dry_call)
10
12
  UnsatisfyingCall.new(
11
13
  call: dry_call,
12
14
  other_stubbings: Mocktail.cabinet.stubbings.select { |stubbing|
13
- dry_call.double == stubbing.recording.double &&
14
- dry_call.method == stubbing.recording.method
15
+ @compares_safely.compare(dry_call.double, stubbing.recording.double) &&
16
+ @compares_safely.compare(dry_call.method, stubbing.recording.method)
15
17
  },
16
18
  backtrace: @cleans_backtrace.clean(Error.new).backtrace
17
19
  )
@@ -41,7 +41,10 @@ module Mocktail
41
41
  def define_double_methods!(dry_class, type, instance_methods)
42
42
  handles_dry_call = @handles_dry_call
43
43
  instance_methods.each do |method|
44
+ dry_class.undef_method(method) if dry_class.method_defined?(method)
45
+
44
46
  dry_class.define_method method, ->(*args, **kwargs, &block) {
47
+ Debug.guard_against_mocktail_accidentally_calling_mocks_if_debugging!
45
48
  handles_dry_call.handle(Call.new(
46
49
  singleton: false,
47
50
  double: self,
@@ -4,7 +4,9 @@ module Mocktail::Matchers
4
4
  :any
5
5
  end
6
6
 
7
- def initialize
7
+ # Change this comment to a descriptive one once this is merged:
8
+ # https://github.com/rubocop/rubocop/pull/10551
9
+ def initialize # standard:disable Style/RedundantInitialize
8
10
  end
9
11
 
10
12
  def match?(actual)
@@ -4,7 +4,9 @@ module Mocktail::Matchers
4
4
  :numeric
5
5
  end
6
6
 
7
- def initialize
7
+ # Change this comment to a descriptive one once this is merged:
8
+ # https://github.com/rubocop/rubocop/pull/10551
9
+ def initialize # standard:disable Style/RedundantInitialize
8
10
  end
9
11
 
10
12
  def match?(actual)
@@ -11,14 +11,14 @@ module Mocktail
11
11
  end
12
12
 
13
13
  def call(call)
14
- raise NoMethodError.new <<~MSG
14
+ raise NoMethodError, <<~MSG, caller[1..]
15
15
  No method `#{@stringifies_method_name.stringify(call)}' exists for call:
16
16
 
17
17
  #{@stringifies_call.stringify(call, anonymous_blocks: true, always_parens: true)}
18
18
 
19
19
  Need to define the method? Here's a sample definition:
20
20
 
21
- def #{call.method}#{params(call)}
21
+ def #{"self." if call.singleton}#{call.method}#{params(call)}
22
22
  end
23
23
  #{corrections(call)}
24
24
  MSG
@@ -12,6 +12,7 @@ module Mocktail
12
12
  type.method(name)
13
13
  } - [type_replacement.replacement_new]
14
14
 
15
+ declare_singleton_method_missing_errors!(type)
15
16
  handles_dry_call = @handles_dry_call
16
17
  type_replacement.replacement_methods = type_replacement.original_methods.map { |original_method|
17
18
  type.singleton_class.send(:undef_method, original_method.name)
@@ -35,5 +36,27 @@ module Mocktail
35
36
  type.singleton_method(original_method.name)
36
37
  }
37
38
  end
39
+
40
+ def declare_singleton_method_missing_errors!(type)
41
+ return if type.singleton_methods.include?(:method_missing)
42
+
43
+ raises_neato_no_method_error = RaisesNeatoNoMethodError.new
44
+ type.define_singleton_method :method_missing,
45
+ ->(name, *args, **kwargs, &block) {
46
+ raises_neato_no_method_error.call(
47
+ Call.new(
48
+ singleton: true,
49
+ double: self,
50
+ original_type: type,
51
+ dry_type: self.class,
52
+ method: name,
53
+ original_method: nil,
54
+ args: args,
55
+ kwargs: kwargs,
56
+ block: block
57
+ )
58
+ )
59
+ }
60
+ end
38
61
  end
39
62
  end
@@ -0,0 +1,7 @@
1
+ module Mocktail
2
+ class ComparesSafely
3
+ def compare(thing, other_thing)
4
+ Object.instance_method(:==).bind_call(thing, other_thing)
5
+ end
6
+ end
7
+ end
@@ -1,8 +1,14 @@
1
+ require_relative "compares_safely"
2
+
1
3
  module Mocktail
2
4
  class DeterminesMatchingCalls
5
+ def initialize
6
+ @compares_safely = ComparesSafely.new
7
+ end
8
+
3
9
  def determine(real_call, demo_call, demo_config)
4
- real_call.double == demo_call.double &&
5
- real_call.method == demo_call.method &&
10
+ @compares_safely.compare(real_call.double, demo_call.double) &&
11
+ @compares_safely.compare(real_call.method, demo_call.method) &&
6
12
 
7
13
  # Matcher implementation will replace this:
8
14
  args_match?(real_call.args, demo_call.args, demo_config.ignore_extra_args) &&
@@ -53,7 +59,7 @@ module Mocktail
53
59
  demo_arg.is_mocktail_matcher?
54
60
  demo_arg.match?(real_arg)
55
61
  else
56
- demo_arg == real_arg
62
+ demo_arg == real_arg # TODO <-- test if mock object and call safe compare if so, otherwise ==
57
63
  end
58
64
  end
59
65
  end
@@ -1,16 +1,22 @@
1
+ require_relative "../share/compares_safely"
2
+
1
3
  module Mocktail
2
4
  class TransformsParams
5
+ def initialize
6
+ @compares_safely = ComparesSafely.new
7
+ end
8
+
3
9
  def transform(dry_call)
4
10
  params = dry_call.original_method.parameters
5
11
 
6
12
  Signature.new(
7
13
  positional_params: Params.new(
8
- all: params.select { |type, _|
9
- [:req, :opt, :rest].include?(type)
14
+ all: params.select { |t, _|
15
+ [:req, :opt, :rest].any? { |param_type| @compares_safely.compare(t, param_type) }
10
16
  }.map { |_, name| name },
11
- required: params.select { |t, _| t == :req }.map { |_, n| n },
12
- optional: params.select { |t, _| t == :opt }.map { |_, n| n },
13
- rest: params.find { |type, _| type == :rest } & [1]
17
+ required: params.select { |t, _| @compares_safely.compare(t, :req) }.map { |_, n| n },
18
+ optional: params.select { |t, _| @compares_safely.compare(t, :opt) }.map { |_, n| n },
19
+ rest: params.find { |t, _| @compares_safely.compare(t, :rest) } & [1]
14
20
  ),
15
21
  positional_args: dry_call.args,
16
22
 
@@ -18,13 +24,13 @@ module Mocktail
18
24
  all: params.select { |type, _|
19
25
  [:keyreq, :key, :keyrest].include?(type)
20
26
  }.map { |_, name| name },
21
- required: params.select { |t, _| t == :keyreq }.map { |_, n| n },
22
- optional: params.select { |t, _| t == :key }.map { |_, n| n },
23
- rest: params.find { |type, _| type == :keyrest } & [1]
27
+ required: params.select { |t, _| @compares_safely.compare(t, :keyreq) }.map { |_, n| n },
28
+ optional: params.select { |t, _| @compares_safely.compare(t, :key) }.map { |_, n| n },
29
+ rest: params.find { |t, _| @compares_safely.compare(t, :keyrest) } & [1]
24
30
  ),
25
31
  keyword_args: dry_call.kwargs,
26
32
 
27
- block_param: params.find { |type, _| type == :block } & [1],
33
+ block_param: params.find { |t, _| @compares_safely.compare(t, :block) } & [1],
28
34
  block_arg: dry_call.block
29
35
  )
30
36
  end
@@ -1,3 +1,5 @@
1
+ require_relative "../share/compares_safely"
2
+
1
3
  # The Cabinet stores all thread-local state, so anything that goes here
2
4
  # is guaranteed by Mocktail to be local to the currently-running thread
3
5
  module Mocktail
@@ -6,6 +8,7 @@ module Mocktail
6
8
  attr_reader :calls, :stubbings, :unsatisfying_calls
7
9
 
8
10
  def initialize
11
+ @compares_safely = ComparesSafely.new
9
12
  @doubles = []
10
13
  @calls = []
11
14
  @stubbings = []
@@ -45,15 +48,19 @@ module Mocktail
45
48
  end
46
49
 
47
50
  def double_for_instance(thing)
48
- @doubles.find { |double| double.dry_instance == thing }
51
+ @doubles.find { |double| @compares_safely.compare(double.dry_instance, thing) }
49
52
  end
50
53
 
51
54
  def stubbings_for_double(double)
52
- @stubbings.select { |stubbing| stubbing.recording.double == double.dry_instance }
55
+ @stubbings.select { |stubbing|
56
+ @compares_safely.compare(stubbing.recording.double, double.dry_instance)
57
+ }
53
58
  end
54
59
 
55
60
  def calls_for_double(double)
56
- @calls.select { |call| call.double == double.dry_instance }
61
+ @calls.select { |call|
62
+ @compares_safely.compare(call.double, double.dry_instance)
63
+ }
57
64
  end
58
65
  end
59
66
  end
@@ -23,4 +23,7 @@ module Mocktail
23
23
 
24
24
  class ReplacedTypeExplanation < Explanation
25
25
  end
26
+
27
+ class FakeMethodExplanation < Explanation
28
+ end
26
29
  end
@@ -0,0 +1,9 @@
1
+ module Mocktail
2
+ class FakeMethodData < Struct.new(
3
+ :receiver,
4
+ :calls,
5
+ :stubbings,
6
+ keyword_init: true
7
+ )
8
+ end
9
+ end
@@ -4,6 +4,7 @@ require_relative "value/demo_config"
4
4
  require_relative "value/double"
5
5
  require_relative "value/double_data"
6
6
  require_relative "value/explanation"
7
+ require_relative "value/fake_method_data"
7
8
  require_relative "value/matcher_registry"
8
9
  require_relative "value/signature"
9
10
  require_relative "value/stubbing"
@@ -1,3 +1,3 @@
1
1
  module Mocktail
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.2"
3
3
  end
data/lib/mocktail.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require_relative "mocktail/debug"
1
2
  require_relative "mocktail/dsl"
2
3
  require_relative "mocktail/errors"
3
4
  require_relative "mocktail/explains_thing"
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: 1.0.0
4
+ version: 1.1.2
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-12-20 00:00:00.000000000 Z
11
+ date: 2022-06-25 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -29,6 +29,7 @@ files:
29
29
  - bin/console
30
30
  - bin/setup
31
31
  - lib/mocktail.rb
32
+ - lib/mocktail/debug.rb
32
33
  - lib/mocktail/dsl.rb
33
34
  - lib/mocktail/errors.rb
34
35
  - lib/mocktail/explains_nils.rb
@@ -67,6 +68,7 @@ files:
67
68
  - lib/mocktail/replaces_type/redefines_singleton_methods.rb
68
69
  - lib/mocktail/resets_state.rb
69
70
  - lib/mocktail/share/cleans_backtrace.rb
71
+ - lib/mocktail/share/compares_safely.rb
70
72
  - lib/mocktail/share/creates_identifier.rb
71
73
  - lib/mocktail/share/determines_matching_calls.rb
72
74
  - lib/mocktail/share/stringifies_call.rb
@@ -82,6 +84,7 @@ files:
82
84
  - lib/mocktail/value/double.rb
83
85
  - lib/mocktail/value/double_data.rb
84
86
  - lib/mocktail/value/explanation.rb
87
+ - lib/mocktail/value/fake_method_data.rb
85
88
  - lib/mocktail/value/matcher_registry.rb
86
89
  - lib/mocktail/value/signature.rb
87
90
  - lib/mocktail/value/stubbing.rb
@@ -116,7 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
116
119
  - !ruby/object:Gem::Version
117
120
  version: '0'
118
121
  requirements: []
119
- rubygems_version: 3.2.22
122
+ rubygems_version: 3.3.6
120
123
  signing_key:
121
124
  specification_version: 4
122
125
  summary: Take your objects, and make them a double