mocktail 0.0.4 → 1.1.0

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: 426f5dbb08f6db2542f0a323d186df7126aaae652e848b0a0c378a602231d777
4
- data.tar.gz: 898501920257accc975d441a12d9e54a034c40aef8c950be2f0fa80aeb0288c6
3
+ metadata.gz: 6a9c74cd3ff5167b99c38892ee6411a1ac3f1379158c062076bfbe6d8479c7b7
4
+ data.tar.gz: 81dd2b80a78c162a9c4030fa8bc9be42741c3a8c053fa33c319dc13c404234a8
5
5
  SHA512:
6
- metadata.gz: 6eea3625f3851c035e00c1593e91ae7d97c76b499510a71bd8b68fb0cfb27c4cac9116f9224f30853a97cfb6826385338506e33bf2ea6d43160f12093e8a591c
7
- data.tar.gz: 1e6041f4ea59a42c40b6ef42bf99f106f43eb18d3ec452580784072046f099af26753e61e79704679fba7973bc19b59b3b9cdf0b537fd95e20caceeb18a719ef
6
+ metadata.gz: bd1fbc482135e89dea00de9249d63b5e851d64ecb2f4afc898b4bb77abf5afc96303cfed6b59665d0e51ab4b2c8dd4c438dc2a7ab93d1c69e1d12d723797cf9a
7
+ data.tar.gz: 3af137dd6d1488b1ef41ac0b47178a224e160f11dce0ed0cbeb24418ae4a33c86a6e4fa8e3cabb20c8421c8ac1ac8c166045051d996d4aa1dc709e4fcc97ec3a
@@ -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,4 +1,28 @@
1
- # unreleased
1
+ # 1.1.0
2
+
3
+ * Feature: add support for passing methods to `Mocktail.explain()`
4
+ * Fix 3.1 support by bypassing highlight_error for custom NoMethodError objects
5
+ raised by Mocktail [error_highlight#20](https://github.com/ruby/error_highlight/issues/20)
6
+
7
+ # 1.0.0
8
+
9
+ * First breaking change! 🎉
10
+ * Remove support for `Mocktail.explain(nil)` because fake nil values cannot be
11
+ made falsey. Pretty big mistake
12
+ * Add `Mocktail.explain_nils` which will return explanation objects of every
13
+ call that didn't satisfy a stubbing since the last reset, including the call
14
+ site where it happened and the backtrace to try to tease out which one you're
15
+ looking for
16
+
17
+ # 0.0.6
18
+
19
+ * Require pathname, which I missed because `bundle exec` loads it. Wups!
20
+
21
+ # 0.0.5
22
+
23
+ * Fix concurrency [#6](https://github.com/testdouble/mocktail/pull/6)
24
+
25
+ # 0.0.4
2
26
 
3
27
  * Introduce Mocktail.explain(), which will return a message & reference object
4
28
  for any of:
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mocktail (0.0.4)
4
+ mocktail (1.1.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -10,29 +10,29 @@ GEM
10
10
  coderay (1.1.3)
11
11
  docile (1.4.0)
12
12
  method_source (1.0.0)
13
- minitest (5.14.4)
13
+ minitest (5.15.0)
14
14
  parallel (1.21.0)
15
- parser (3.0.2.0)
15
+ parser (3.1.0.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.1.1)
22
+ regexp_parser (2.2.0)
23
23
  rexml (3.2.5)
24
- rubocop (1.20.0)
24
+ rubocop (1.25.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.9.1, < 2.0)
30
+ rubocop-ast (>= 1.15.1, < 2.0)
31
31
  ruby-progressbar (~> 1.7)
32
32
  unicode-display_width (>= 1.4.0, < 3.0)
33
- rubocop-ast (1.11.0)
33
+ rubocop-ast (1.15.1)
34
34
  parser (>= 3.0.1.1)
35
- rubocop-performance (1.11.5)
35
+ rubocop-performance (1.13.2)
36
36
  rubocop (>= 1.7.0, < 2.0)
37
37
  rubocop-ast (>= 0.4.0)
38
38
  ruby-progressbar (1.11.0)
@@ -42,9 +42,9 @@ GEM
42
42
  simplecov_json_formatter (~> 0.1)
43
43
  simplecov-html (0.12.3)
44
44
  simplecov_json_formatter (0.1.3)
45
- standard (1.3.0)
46
- rubocop (= 1.20.0)
47
- rubocop-performance (= 1.11.5)
45
+ standard (1.7.0)
46
+ rubocop (= 1.25.0)
47
+ rubocop-performance (= 1.13.2)
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.15
63
+ 2.3.6
data/README.md CHANGED
@@ -553,92 +553,42 @@ them!)]
553
553
  ### Mocktail.explain
554
554
 
555
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.
556
+ which way, so Mocktail tries to make it a little easier on you. In addition to
557
+ returning useful messages throughout the API, the gem also includes an
558
+ introspection method `Mocktail.explain(thing)`, which returns a human-readable
559
+ `message` and a `reference` object with useful attributes (that vary depending
560
+ on the type of fake `thing` you pass in. Below are some things `explain()` can
561
+ do.
558
562
 
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.
563
+ #### Fake instances created by Mocktail
584
564
 
585
- #### `nil` values returned by faked methods
565
+ Any instances created by `Mocktail.of` or `Mocktail.of_next` can be passed to
566
+ `Mocktail.explain`, and they will list out all the calls and stubbings made for
567
+ each of their fake methods.
586
568
 
587
- Suppose you go ahead and implement the `fill` method above and configure a
588
- stubbing:
569
+ Suppose these interactions have occurred:
589
570
 
590
571
  ```ruby
591
572
  ice_tray = Mocktail.of(IceTray)
592
573
 
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:
574
+ Mocktail.stubs { ice_tray.fill(:tap_water, 30) }.with { :some_ice }
598
575
 
599
- ```ruby
600
- def prep
601
- ice = ice_tray.fill(:tap_water, 50) # => nil
602
- glass.add(ice)
603
- end
576
+ ice_tray.fill(:tap_water, 50)
604
577
  ```
605
578
 
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:
579
+ You can interrogate what's going on with the fake instance by passing it to
580
+ `explain`:
609
581
 
610
582
  ```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:
583
+ explanation = Mocktail.explain(ice_tray)
626
584
 
627
- fill(:tap_water, 50)
628
-
629
- Stubbings configured prior to this call but not satisfied by it:
630
-
631
- fill(:tap_water, 30)
585
+ explanation.reference.type #=> IceTray
586
+ explanation.reference.double #=> The ice_tray instance
587
+ explanation.reference.calls #=> details on each invocation of each method
588
+ explanation.reference.stubbings #=> all stubbings configured for each method
632
589
  ```
633
590
 
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:
591
+ Calling `explanation.message` will return:
642
592
 
643
593
  ```
644
594
  This is a fake `IceTray' instance.
@@ -653,6 +603,7 @@ It has these mocked methods:
653
603
  `IceTray#fill' calls:
654
604
 
655
605
  fill(:tap_water, 50)
606
+
656
607
  ```
657
608
 
658
609
  #### Modules and classes with singleton methods replaced
@@ -662,6 +613,8 @@ passed to `Mocktail.explain()` for a summary of all the stubbing configurations
662
613
  and calls made against its faked singleton methods for the currently running
663
614
  thread.
664
615
 
616
+ Imagine a `Shop` class with `self.open!` and `self.close!` singleton methods:
617
+
665
618
  ```ruby
666
619
  Mocktail.replace(Shop)
667
620
 
@@ -671,10 +624,15 @@ Shop.open!(42)
671
624
 
672
625
  Shop.close!(42)
673
626
 
674
- puts Mocktail.explain(Shop).message
627
+ explanation = Mocktail.explain(Shop)
628
+
629
+ explanation.reference.type #=> Shop
630
+ explanation.reference.replaced_method_names #=> [:close!, :open!]
631
+ explanation.reference.calls #=> details on each invocation of each method
632
+ explanation.reference.stubbings #=> all stubbings configured for each method
675
633
  ```
676
634
 
677
- Will print:
635
+ And `explanation.message` will return:
678
636
 
679
637
  ```ruby
680
638
  `Shop' is a class that has had its singleton methods faked.
@@ -689,15 +647,161 @@ It has these mocked methods:
689
647
 
690
648
  close!(42)
691
649
 
650
+ close!(42)
651
+
692
652
  `Shop.open!' stubbings:
693
653
 
694
654
  open!(numeric)
695
655
 
656
+ open!(numeric)
657
+
696
658
  `Shop.open!' calls:
697
659
 
698
660
  open!(42)
661
+
662
+ open!(42)
663
+ ```
664
+
665
+ #### Methods on faked instances and replaced types
666
+
667
+ In addition to passing the test double, you can also pass a reference to any
668
+ fake method created by Mocktail to `Mocktail.explain`:
669
+
670
+ ```ruby
671
+ ice_tray = Mocktail.of(IceTray)
672
+
673
+ ice_tray.fill(:chilled, 50)
674
+
675
+ explanation = Mocktail.explain(ice_tray.method(:fill))
676
+
677
+ explanation.reference.receiver #=> a reference to the `ice_tray` instance
678
+ explanation.reference.calls #=> details on each invocation of the method
679
+ explanation.reference.stubbings #=> all stubbings configured for the method
680
+ ```
681
+
682
+ The above may be handy in cases where you want to assert the number of calls of
683
+ a method outside the `Mocktail.verify` API:
684
+
685
+ ```ruby
686
+ assert_equal 1, explanation.reference.calls.size
687
+ ```
688
+
689
+ The explanation will also contain a `message` like this:
690
+
691
+ ```
692
+ `IceTray#fill' has no stubbings.
693
+
694
+ `IceTray#fill' calls:
695
+
696
+ fill(:chilled, 50)
697
+ ```
698
+
699
+ Replaced singleton methods can also be passed to `explain()`, so something like
700
+ `Mocktail.explain(Shop.method(:open!))` from the earlier example would also work
701
+ (with `Shop` being the `receiver` on the explanation's `reference`).
702
+
703
+ #### Undefined methods
704
+
705
+ There's no API for this one, but Mocktail also offers explanations for methods
706
+ that don't exist yet. You'll see this error message whenever you try to call a
707
+ method that doesn't exist on a test double. The message is designed to
708
+ facilitate "paint-by-numbers" TDD, by including a sample definition of the
709
+ method you had attempted to call that can be copy-pasted into a source listing:
710
+
711
+ ```ruby
712
+ class IceTray
713
+ end
714
+
715
+ ice_tray = Mocktail.of(IceTray)
716
+
717
+ ice_tray.fill(:water_type, 30)
718
+ # => No method `IceTray#fill' exists for call: (NoMethodError)
719
+ #
720
+ # fill(:water_type, 30)
721
+ #
722
+ # Need to define the method? Here's a sample definition:
723
+ #
724
+ # def fill(water_type, arg)
725
+ # end
726
+ ```
727
+
728
+ From there, you can just copy-paste the provided method stub as a starting point
729
+ for your new method:
730
+
731
+ ```ruby
732
+ class IceTray
733
+ def fill(water_type, amount)
734
+ end
735
+ end
736
+ ```
737
+
738
+ ### Mocktail.explain_nils
739
+
740
+ Is a faked method returning `nil` and you don't understand why?
741
+
742
+ By default, methods faked by Mocktail will return `nil` when no stubbing is
743
+ satisfied. A frequent frustration, therefore, is when the way `stubs {}.with {}`
744
+ is configured does not satisfy a call the way you expected. To try to make
745
+ debugging this a little bit easier, the gem provides a top-level
746
+ `Mocktail.explain_nils` method that will return an array of summaries of every
747
+ call to a faked method that failed to satisfy any stubbings.
748
+
749
+ For example, suppose you stub this `fill` method like so:
750
+
751
+ ```ruby
752
+ ice_tray = Mocktail.of(IceTray)
753
+
754
+ stubs { ice_tray.fill(:tap_water, 30) }.with { :normal_ice }
755
+ ```
756
+
757
+ But then you find that your subject under test is just getting `nil` back and
758
+ you don't understand why:
759
+
760
+ ```ruby
761
+ def prep
762
+ ice = ice_tray.fill(:tap_water, 50)
763
+ glass.add(ice) # => why is `ice` nil?!
764
+ end
765
+ ```
766
+
767
+ Whenever you're confused by a nil, you can call `Mocktail.explain_nils` for an
768
+ array containing `UnsatisfyingCallExplanation` objects (one for each call to
769
+ a faked method that did not satisfy any configured stubbings).
770
+
771
+ The returned explanation objects will include both a `reference` object to
772
+ explore as well a summary `message`:
773
+
774
+ ```ruby
775
+ def prep
776
+ ice = ice_tray.fill(:tap_water, 50)
777
+ puts Mocktail.explain_nils.first.message
778
+ glass.add(ice)
779
+ end
699
780
  ```
700
781
 
782
+ Which will print:
783
+
784
+ ```
785
+ This `nil' was returned by a mocked `IceTray#fill' method
786
+ because none of its configured stubbings were satisfied.
787
+
788
+ The actual call:
789
+
790
+ fill(:tap_water, 50)
791
+
792
+ The call site:
793
+
794
+ /path/to/your/code.rb:42:in `prep'
795
+
796
+ Stubbings configured prior to this call but not satisfied by it:
797
+
798
+ fill(:tap_water, 30)
799
+ ```
800
+
801
+ The `reference` object will have details of the `call` itself, an array of
802
+ `other_stubbings` defined on the faked method, and a `backtrace` to determine
803
+ which call site produced the unexpected `nil` value.
804
+
701
805
  ### Mocktail.reset
702
806
 
703
807
  This one's simple: you probably want to call `Mocktail.reset` after each test,
@@ -705,6 +809,10 @@ but you _definitely_ want to call it if you're using `Mocktail.replace` or
705
809
  `Mocktail.of_next` anywhere, since those will affect state that is shared across
706
810
  tests.
707
811
 
812
+ Calling reset in a `teardown` or `after(:each)` hook will also improve the
813
+ usefulness of messages returned by `Mocktail.explain` and
814
+ `Mocktail.explain_nils`.
815
+
708
816
  ## Acknowledgements
709
817
 
710
818
  Mocktail is created & maintained by the software agency [Test
@@ -731,3 +839,4 @@ including (but not limited to) one-on-one communications, public posts/comments,
731
839
  code reviews, pull requests, and GitHub issues. If violations occur, Test Double
732
840
  will take any action they deem appropriate for the infraction, up to and
733
841
  including blocking a user from the organization's repositories.
842
+
@@ -0,0 +1,35 @@
1
+ require_relative "share/stringifies_method_name"
2
+ require_relative "share/stringifies_call"
3
+
4
+ module Mocktail
5
+ class ExplainsNils
6
+ def initialize
7
+ @stringifies_method_name = StringifiesMethodName.new
8
+ @stringifies_call = StringifiesCall.new
9
+ end
10
+
11
+ def explain
12
+ Mocktail.cabinet.unsatisfying_calls.map { |unsatisfying_call|
13
+ dry_call = unsatisfying_call.call
14
+ other_stubbings = unsatisfying_call.other_stubbings
15
+
16
+ UnsatisfyingCallExplanation.new(unsatisfying_call, <<~MSG)
17
+ `nil' was returned by a mocked `#{@stringifies_method_name.stringify(dry_call)}' method
18
+ because none of its configured stubbings were satisfied.
19
+
20
+ The actual call:
21
+
22
+ #{@stringifies_call.stringify(dry_call, always_parens: true)}
23
+
24
+ The call site:
25
+
26
+ #{unsatisfying_call.backtrace.first}
27
+
28
+ #{@stringifies_call.stringify_multiple(other_stubbings.map(&:recording),
29
+ nonzero_message: "Stubbings configured prior to this call but not satisfied by it",
30
+ zero_message: "No stubbings were configured on this method")}
31
+ MSG
32
+ }
33
+ end
34
+ end
35
+ end
@@ -9,12 +9,12 @@ module Mocktail
9
9
  end
10
10
 
11
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))
12
+ if (double = Mocktail.cabinet.double_for_instance(thing))
15
13
  double_explanation(double)
16
14
  elsif (type_replacement = TopShelf.instance.type_replacement_if_exists_for(thing))
17
15
  replaced_type_explanation(type_replacement)
16
+ elsif (fake_method_explanation = fake_method_explanation_for(thing))
17
+ fake_method_explanation
18
18
  else
19
19
  no_explanation(thing)
20
20
  end
@@ -22,51 +22,50 @@ module Mocktail
22
22
 
23
23
  private
24
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.
25
+ def fake_method_explanation_for(thing)
26
+ return unless thing.is_a?(Method)
27
+ method = thing
28
+ receiver = thing.receiver
39
29
 
40
- The actual call:
41
-
42
- #{@stringifies_call.stringify(dry_call, always_parens: true)}
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
43
35
 
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
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
48
43
  end
49
44
 
50
- def double_explanation(double)
51
- double_data = DoubleData.new(
45
+ def data_for_double(double)
46
+ DoubleData.new(
52
47
  type: double.original_type,
53
48
  double: double.dry_instance,
54
49
  calls: Mocktail.cabinet.calls_for_double(double),
55
50
  stubbings: Mocktail.cabinet.stubbings_for_double(double)
56
51
  )
52
+ end
53
+
54
+ def double_explanation(double)
55
+ double_data = data_for_double(double)
57
56
 
58
57
  DoubleExplanation.new(double_data, <<~MSG)
59
58
  This is a fake `#{double.original_type.name}' instance.
60
59
 
61
60
  It has these mocked methods:
62
- #{double.dry_methods.map { |method| " - #{method}" }.join("\n")}
61
+ #{double.dry_methods.sort.map { |method| " - #{method}" }.join("\n")}
63
62
 
64
- #{double.dry_methods.map { |method| describe_dry_method(double_data, method) }.join("\n")}
63
+ #{double.dry_methods.sort.map { |method| describe_dry_method(double_data, method) }.join("\n")}
65
64
  MSG
66
65
  end
67
66
 
68
- def replaced_type_explanation(type_replacement)
69
- type_replacement_data = TypeReplacementData.new(
67
+ def data_for_type_replacement(type_replacement)
68
+ TypeReplacementData.new(
70
69
  type: type_replacement.type,
71
70
  replaced_method_names: type_replacement.replacement_methods.map(&:name).sort,
72
71
  calls: Mocktail.cabinet.calls.select { |call|
@@ -76,6 +75,10 @@ module Mocktail
76
75
  stubbing.recording.double == type_replacement.type
77
76
  }
78
77
  )
78
+ end
79
+
80
+ def replaced_type_explanation(type_replacement)
81
+ type_replacement_data = data_for_type_replacement(type_replacement)
79
82
 
80
83
  ReplacedTypeExplanation.new(type_replacement_data, <<~MSG)
81
84
  `#{type_replacement.type}' is a #{type_replacement.type.class.to_s.downcase} that has had its singleton methods faked.
@@ -95,35 +98,23 @@ module Mocktail
95
98
  ))
96
99
 
97
100
  [
98
- describe_multiple_calls(
101
+ @stringifies_call.stringify_multiple(
99
102
  double_data.stubbings.map(&:recording).select { |call|
100
103
  call.method == method
101
104
  },
102
- "`#{method_name}' stubbings",
103
- "`#{method_name}' has no stubbings"
105
+ nonzero_message: "`#{method_name}' stubbings",
106
+ zero_message: "`#{method_name}' has no stubbings"
104
107
  ),
105
- describe_multiple_calls(
108
+ @stringifies_call.stringify_multiple(
106
109
  double_data.calls.select { |call|
107
110
  call.method == method
108
111
  },
109
- "`#{method_name}' calls",
110
- "`#{method_name}' has no calls"
112
+ nonzero_message: "`#{method_name}' calls",
113
+ zero_message: "`#{method_name}' has no calls"
111
114
  )
112
115
  ].join("\n")
113
116
  end
114
117
 
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
118
  def no_explanation(thing)
128
119
  NoExplanation.new(thing,
129
120
  "Unfortunately, Mocktail doesn't know what this thing is: #{thing.inspect}")
@@ -1,12 +1,19 @@
1
+ require_relative "../../share/cleans_backtrace"
2
+
1
3
  module Mocktail
2
4
  class DescribesUnsatisfiedStubbing
5
+ def initialize
6
+ @cleans_backtrace = CleansBacktrace.new
7
+ end
8
+
3
9
  def describe(dry_call)
4
- UnsatisfiedStubbing.new(
10
+ UnsatisfyingCall.new(
5
11
  call: dry_call,
6
12
  other_stubbings: Mocktail.cabinet.stubbings.select { |stubbing|
7
- dry_call.double == stubbing.recording.double &&
8
- dry_call.method == stubbing.recording.method
9
- }
13
+ dry_call.double == stubbing.recording.double &&
14
+ dry_call.method == stubbing.recording.method
15
+ },
16
+ backtrace: @cleans_backtrace.clean(Error.new).backtrace
10
17
  )
11
18
  end
12
19
  end
@@ -13,13 +13,25 @@ module Mocktail
13
13
  stubbing.satisfied!
14
14
  stubbing.effect&.call(dry_call)
15
15
  else
16
- StubReturnedNil.new(@describes_unsatisfied_stubbing.describe(dry_call))
16
+ store_unsatisfying_call!(dry_call)
17
+ nil
17
18
  end
18
19
  end
19
20
 
20
21
  def satisfaction(dry_call)
21
22
  return if Mocktail.cabinet.demonstration_in_progress?
23
+
22
24
  @finds_satisfaction.find(dry_call)
23
25
  end
26
+
27
+ private
28
+
29
+ def store_unsatisfying_call!(dry_call)
30
+ return if Mocktail.cabinet.demonstration_in_progress?
31
+
32
+ Mocktail.cabinet.store_unsatisfying_call(
33
+ @describes_unsatisfied_stubbing.describe(dry_call)
34
+ )
35
+ end
24
36
  end
25
37
  end
@@ -34,6 +34,10 @@ module Mocktail::Matchers
34
34
  def captured?
35
35
  @captured
36
36
  end
37
+
38
+ def inspect
39
+ "capture"
40
+ end
37
41
  end
38
42
 
39
43
  attr_reader :capture
@@ -6,7 +6,7 @@ module Mocktail::Matchers
6
6
 
7
7
  def initialize(&blk)
8
8
  if blk.nil?
9
- raise "The `that` matcher must be passed a block (e.g. `that { |arg| … }`)"
9
+ raise ArgumentError.new("The `that` matcher must be passed a block (e.g. `that { |arg| … }`)")
10
10
  end
11
11
  @blk = blk
12
12
  end
@@ -11,7 +11,7 @@ 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)}
@@ -1,3 +1,5 @@
1
+ require "pathname"
2
+
1
3
  module Mocktail
2
4
  class CleansBacktrace
3
5
  BASE_PATH = (Pathname.new(__FILE__) + "../../..").to_s
@@ -4,6 +4,20 @@ module Mocktail
4
4
  "#{call.method}#{args_to_s(call, parens: always_parens)}#{blockify(call.block, anonymous: anonymous_blocks)}"
5
5
  end
6
6
 
7
+ def stringify_multiple(calls, nonzero_message:, zero_message:,
8
+ anonymous_blocks: false, always_parens: false)
9
+
10
+ if calls.empty?
11
+ "#{zero_message}.\n"
12
+ else
13
+ <<~MSG
14
+ #{nonzero_message}:
15
+
16
+ #{calls.map { |call| " " + stringify(call) }.join("\n\n")}
17
+ MSG
18
+ end
19
+ end
20
+
7
21
  private
8
22
 
9
23
  def args_to_s(call, parens: true)
@@ -1,7 +1,7 @@
1
1
  require_relative "simulates_argument_error/transforms_params"
2
2
  require_relative "simulates_argument_error/reconciles_args_with_params"
3
3
  require_relative "simulates_argument_error/recreates_message"
4
- require_relative "simulates_argument_error/cleans_backtrace"
4
+ require_relative "share/cleans_backtrace"
5
5
  require_relative "share/stringifies_call"
6
6
 
7
7
  module Mocktail
@@ -3,18 +3,20 @@
3
3
  module Mocktail
4
4
  class Cabinet
5
5
  attr_writer :demonstration_in_progress
6
- attr_reader :calls, :stubbings
6
+ attr_reader :calls, :stubbings, :unsatisfying_calls
7
7
 
8
8
  def initialize
9
9
  @doubles = []
10
10
  @calls = []
11
11
  @stubbings = []
12
+ @unsatisfying_calls = []
12
13
  @demonstration_in_progress = false
13
14
  end
14
15
 
15
16
  def reset!
16
17
  @calls = []
17
18
  @stubbings = []
19
+ @unsatisfying_calls = []
18
20
  # Could cause an exception or prevent pollution—you decide!
19
21
  @demonstration_in_progress = false
20
22
  # note we don't reset doubles as they don't carry any
@@ -34,6 +36,10 @@ module Mocktail
34
36
  @stubbings << stubbing
35
37
  end
36
38
 
39
+ def store_unsatisfying_call(unsatisfying_call)
40
+ @unsatisfying_calls << unsatisfying_call
41
+ end
42
+
37
43
  def demonstration_in_progress?
38
44
  @demonstration_in_progress
39
45
  end
@@ -15,7 +15,7 @@ module Mocktail
15
15
  class NoExplanation < Explanation
16
16
  end
17
17
 
18
- class UnsatisfiedStubExplanation < Explanation
18
+ class UnsatisfyingCallExplanation < Explanation
19
19
  end
20
20
 
21
21
  class DoubleExplanation < Explanation
@@ -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
@@ -1,65 +1,60 @@
1
- # The top shelf stores all cross-thread & thread-aware state, so anything that
2
- # goes here is on its own when it comes to ensuring thread safety.
3
1
  module Mocktail
4
2
  class TopShelf
5
3
  def self.instance
6
- @self ||= new
4
+ Thread.current[:mocktail_top_shelf] ||= new
7
5
  end
8
6
 
7
+ @@type_replacements = {}
8
+ @@type_replacements_mutex = Mutex.new
9
+
9
10
  def initialize
10
- @type_replacements = {}
11
- @new_registrations = {}
12
- @of_next_registrations = {}
13
- @singleton_method_registrations = {}
11
+ @new_registrations = []
12
+ @of_next_registrations = []
13
+ @singleton_method_registrations = []
14
14
  end
15
15
 
16
16
  def type_replacement_for(type)
17
- @type_replacements[type] ||= TypeReplacement.new(type: type)
17
+ @@type_replacements_mutex.synchronize {
18
+ @@type_replacements[type] ||= TypeReplacement.new(type: type)
19
+ }
18
20
  end
19
21
 
20
22
  def type_replacement_if_exists_for(type)
21
- @type_replacements[type]
23
+ @@type_replacements_mutex.synchronize {
24
+ @@type_replacements[type]
25
+ }
22
26
  end
23
27
 
24
28
  def reset_current_thread!
25
- @new_registrations[Thread.current] = []
26
- @of_next_registrations[Thread.current] = []
27
- @singleton_method_registrations[Thread.current] = []
29
+ Thread.current[:mocktail_top_shelf] = self.class.new
28
30
  end
29
31
 
30
32
  def register_new_replacement!(type)
31
- @new_registrations[Thread.current] ||= []
32
- @new_registrations[Thread.current] |= [type]
33
+ @new_registrations |= [type]
33
34
  end
34
35
 
35
36
  def new_replaced?(type)
36
- @new_registrations[Thread.current] ||= []
37
- @new_registrations[Thread.current].include?(type)
37
+ @new_registrations.include?(type)
38
38
  end
39
39
 
40
40
  def register_of_next_replacement!(type)
41
- @of_next_registrations[Thread.current] ||= []
42
- @of_next_registrations[Thread.current] |= [type]
41
+ @of_next_registrations |= [type]
43
42
  end
44
43
 
45
44
  def of_next_registered?(type)
46
- @of_next_registrations[Thread.current] ||= []
47
- @of_next_registrations[Thread.current].include?(type)
45
+ @of_next_registrations.include?(type)
48
46
  end
49
47
 
50
48
  def unregister_of_next_replacement!(type)
51
- @of_next_registrations[Thread.current] ||= []
52
- @of_next_registrations[Thread.current] -= [type]
49
+ @of_next_registrations -= [type]
53
50
  end
54
51
 
55
52
  def register_singleton_method_replacement!(type)
56
- @singleton_method_registrations[Thread.current] ||= []
57
- @singleton_method_registrations[Thread.current] |= [type]
53
+ @singleton_method_registrations |= [type]
58
54
  end
59
55
 
60
56
  def singleton_methods_replaced?(type)
61
- @singleton_method_registrations[Thread.current] ||= []
62
- @singleton_method_registrations[Thread.current].include?(type)
57
+ @singleton_method_registrations.include?(type)
63
58
  end
64
59
  end
65
60
  end
@@ -1,7 +1,8 @@
1
1
  module Mocktail
2
- class UnsatisfiedStubbing < Struct.new(
2
+ class UnsatisfyingCall < Struct.new(
3
3
  :call,
4
4
  :other_stubbings,
5
+ :backtrace,
5
6
  keyword_init: true
6
7
  )
7
8
  end
@@ -4,11 +4,11 @@ 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"
10
- require_relative "value/stub_returned_nil"
11
11
  require_relative "value/top_shelf"
12
12
  require_relative "value/type_replacement"
13
13
  require_relative "value/type_replacement_data"
14
- require_relative "value/unsatisfied_stubbing"
14
+ require_relative "value/unsatisfying_call"
@@ -1,3 +1,3 @@
1
1
  module Mocktail
2
- VERSION = "0.0.4"
2
+ VERSION = "1.1.0"
3
3
  end
data/lib/mocktail.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require_relative "mocktail/dsl"
2
2
  require_relative "mocktail/errors"
3
3
  require_relative "mocktail/explains_thing"
4
+ require_relative "mocktail/explains_nils"
4
5
  require_relative "mocktail/handles_dry_call"
5
6
  require_relative "mocktail/handles_dry_new_call"
6
7
  require_relative "mocktail/imitates_type"
@@ -61,6 +62,10 @@ module Mocktail
61
62
  ExplainsThing.new.explain(thing)
62
63
  end
63
64
 
65
+ def self.explain_nils
66
+ ExplainsNils.new.explain
67
+ end
68
+
64
69
  # Stores most transactional state about calls & stubbing configurations
65
70
  # Anything returned by this is undocumented and could change at any time, so
66
71
  # don't commit code that relies on it!
data/mocktail.gemspec CHANGED
@@ -6,7 +6,7 @@ Gem::Specification.new do |spec|
6
6
  spec.authors = ["Justin Searls"]
7
7
  spec.email = ["searls@gmail.com"]
8
8
 
9
- spec.summary = "your objects, less potency"
9
+ spec.summary = "Take your objects, and make them a double"
10
10
  spec.homepage = "https://github.com/testdouble/mocktail"
11
11
  spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
12
12
 
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
4
+ version: 1.1.0
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-07 00:00:00.000000000 Z
11
+ date: 2022-01-27 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -31,6 +31,7 @@ files:
31
31
  - lib/mocktail.rb
32
32
  - lib/mocktail/dsl.rb
33
33
  - lib/mocktail/errors.rb
34
+ - lib/mocktail/explains_nils.rb
34
35
  - lib/mocktail/explains_thing.rb
35
36
  - lib/mocktail/handles_dry_call.rb
36
37
  - lib/mocktail/handles_dry_call/fulfills_stubbing.rb
@@ -65,12 +66,12 @@ files:
65
66
  - lib/mocktail/replaces_type/redefines_new.rb
66
67
  - lib/mocktail/replaces_type/redefines_singleton_methods.rb
67
68
  - lib/mocktail/resets_state.rb
69
+ - lib/mocktail/share/cleans_backtrace.rb
68
70
  - lib/mocktail/share/creates_identifier.rb
69
71
  - lib/mocktail/share/determines_matching_calls.rb
70
72
  - lib/mocktail/share/stringifies_call.rb
71
73
  - lib/mocktail/share/stringifies_method_name.rb
72
74
  - lib/mocktail/simulates_argument_error.rb
73
- - lib/mocktail/simulates_argument_error/cleans_backtrace.rb
74
75
  - lib/mocktail/simulates_argument_error/reconciles_args_with_params.rb
75
76
  - lib/mocktail/simulates_argument_error/recreates_message.rb
76
77
  - lib/mocktail/simulates_argument_error/transforms_params.rb
@@ -81,14 +82,14 @@ files:
81
82
  - lib/mocktail/value/double.rb
82
83
  - lib/mocktail/value/double_data.rb
83
84
  - lib/mocktail/value/explanation.rb
85
+ - lib/mocktail/value/fake_method_data.rb
84
86
  - lib/mocktail/value/matcher_registry.rb
85
87
  - lib/mocktail/value/signature.rb
86
- - lib/mocktail/value/stub_returned_nil.rb
87
88
  - lib/mocktail/value/stubbing.rb
88
89
  - lib/mocktail/value/top_shelf.rb
89
90
  - lib/mocktail/value/type_replacement.rb
90
91
  - lib/mocktail/value/type_replacement_data.rb
91
- - lib/mocktail/value/unsatisfied_stubbing.rb
92
+ - lib/mocktail/value/unsatisfying_call.rb
92
93
  - lib/mocktail/verifies_call.rb
93
94
  - lib/mocktail/verifies_call/finds_verifiable_calls.rb
94
95
  - lib/mocktail/verifies_call/raises_verification_error.rb
@@ -116,8 +117,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
116
117
  - !ruby/object:Gem::Version
117
118
  version: '0'
118
119
  requirements: []
119
- rubygems_version: 3.2.15
120
+ rubygems_version: 3.3.6
120
121
  signing_key:
121
122
  specification_version: 4
122
- summary: your objects, less potency
123
+ summary: Take your objects, and make them a double
123
124
  test_files: []
@@ -1,26 +0,0 @@
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