mocktail 1.1.0 → 1.1.3

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: 6a9c74cd3ff5167b99c38892ee6411a1ac3f1379158c062076bfbe6d8479c7b7
4
- data.tar.gz: 81dd2b80a78c162a9c4030fa8bc9be42741c3a8c053fa33c319dc13c404234a8
3
+ metadata.gz: 0aa187046d2e07280352ba6f0296f5573bda910acb111e06d0932f0e2518064e
4
+ data.tar.gz: c3e5e74d586969ea5dd88dc86c20a9d2e6f223f1bb8a438325583a8d8fb20bae
5
5
  SHA512:
6
- metadata.gz: bd1fbc482135e89dea00de9249d63b5e851d64ecb2f4afc898b4bb77abf5afc96303cfed6b59665d0e51ab4b2c8dd4c438dc2a7ab93d1c69e1d12d723797cf9a
7
- data.tar.gz: 3af137dd6d1488b1ef41ac0b47178a224e160f11dce0ed0cbeb24418ae4a33c86a6e4fa8e3cabb20c8421c8ac1ac8c166045051d996d4aa1dc709e4fcc97ec3a
6
+ metadata.gz: 224a377d46c11b830b90a828e6a0c294e940c3492cde42fda9aa0f865b97cdc69773571d4464bd4bbe185aa29e6860c5a4c2b3b87a458382a84b330ddce5df3e
7
+ data.tar.gz: 10562536842265afc7f6bef9fa63a8ebab657e78941f13bc0e38cb49cf8f43785d1d926d0f5afe103165d337fe2163cba88aa4aee220004c8a2494e5f5493d4c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ # 1.1.3
2
+
3
+ * Improve the robustness of how we call the original methods on doubles &
4
+ replaced class methods on mocks internally by Mocktail to avoid behavior being
5
+ altered by how users configure mock objects
6
+
7
+ # 1.1.2
8
+
9
+ * Fix cases where classes that redefine built-in methods could cause issues when
10
+ Mocktail in turn called those methods internally
11
+ [#15](https://github.com/testdouble/mocktail/pull/15)
12
+
13
+ # 1.1.1
14
+
15
+ * Improve output for undefined singleton methods
16
+ ([#11](https://github.com/testdouble/mocktail/pull/11) by
17
+ [@calebhearth](https://github.com/calebhearth))
18
+
1
19
  # 1.1.0
2
20
 
3
21
  * Feature: add support for passing methods to `Mocktail.explain()`
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mocktail (1.1.0)
4
+ mocktail (1.1.3)
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.1.0.0)
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
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.25.0)
24
+ rubocop (1.27.0)
25
25
  parallel (~> 1.10)
26
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.15.1, < 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.1)
34
- parser (>= 3.0.1.1)
35
- rubocop-performance (1.13.2)
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.7.0)
46
- rubocop (= 1.25.0)
47
- rubocop-performance (= 1.13.2)
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
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
@@ -813,10 +818,35 @@ Calling reset in a `teardown` or `after(:each)` hook will also improve the
813
818
  usefulness of messages returned by `Mocktail.explain` and
814
819
  `Mocktail.explain_nils`.
815
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
+
816
846
  ## Acknowledgements
817
847
 
818
848
  Mocktail is created & maintained by the software agency [Test
819
- 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
820
850
  [testdouble.js](https://github.com/testdouble/testdouble.js/), you might find
821
851
  Mocktail's API to be quite similar. The term "test double" was originally coined
822
852
  by Gerard Meszaros in his book [xUnit Test
@@ -839,4 +869,3 @@ including (but not limited to) one-on-one communications, public posts/comments,
839
869
  code reviews, pull requests, and GitHub issues. If violations occur, Test Double
840
870
  will take any action they deem appropriate for the infraction, up to and
841
871
  including blocking a user from the organization's repositories.
842
-
@@ -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
@@ -1,4 +1,5 @@
1
1
  require_relative "../../share/cleans_backtrace"
2
+ require_relative "../../share/bind"
2
3
 
3
4
  module Mocktail
4
5
  class DescribesUnsatisfiedStubbing
@@ -10,7 +11,7 @@ module Mocktail
10
11
  UnsatisfyingCall.new(
11
12
  call: dry_call,
12
13
  other_stubbings: Mocktail.cabinet.stubbings.select { |stubbing|
13
- dry_call.double == stubbing.recording.double &&
14
+ Bind.call(dry_call.double, :==, stubbing.recording.double) &&
14
15
  dry_call.method == stubbing.recording.method
15
16
  },
16
17
  backtrace: @cleans_backtrace.clean(Error.new).backtrace
@@ -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)
@@ -18,7 +18,7 @@ module Mocktail
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,14 @@
1
+ module Mocktail
2
+ module Bind
3
+ def self.call(mock, method_name, *args, **kwargs, &blk)
4
+ if Mocktail.cabinet.double_for_instance(mock)
5
+ Object.instance_method(method_name).bind_call(mock, *args, **kwargs, &blk)
6
+ elsif (type_replacement = TopShelf.instance.type_replacement_if_exists_for(mock)) &&
7
+ (og_method = type_replacement.original_methods&.find { |m| m.name == method_name })
8
+ og_method.call(*args, **kwargs, &blk)
9
+ else
10
+ mock.__send__(method_name, *args, **kwargs, &blk)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -1,7 +1,9 @@
1
+ require_relative "bind"
2
+
1
3
  module Mocktail
2
4
  class DeterminesMatchingCalls
3
5
  def determine(real_call, demo_call, demo_config)
4
- real_call.double == demo_call.double &&
6
+ Bind.call(real_call.double, :==, demo_call.double) &&
5
7
  real_call.method == demo_call.method &&
6
8
 
7
9
  # Matcher implementation will replace this:
@@ -49,11 +51,11 @@ module Mocktail
49
51
  end
50
52
 
51
53
  def match?(real_arg, demo_arg)
52
- if demo_arg.respond_to?(:is_mocktail_matcher?) &&
54
+ if Bind.call(demo_arg, :respond_to?, :is_mocktail_matcher?) &&
53
55
  demo_arg.is_mocktail_matcher?
54
56
  demo_arg.match?(real_arg)
55
57
  else
56
- demo_arg == real_arg
58
+ Bind.call(demo_arg, :==, real_arg)
57
59
  end
58
60
  end
59
61
  end
@@ -1,3 +1,5 @@
1
+ require_relative "../share/bind"
2
+
1
3
  module Mocktail
2
4
  class TransformsParams
3
5
  def transform(dry_call)
@@ -5,12 +7,12 @@ module Mocktail
5
7
 
6
8
  Signature.new(
7
9
  positional_params: Params.new(
8
- all: params.select { |type, _|
9
- [:req, :opt, :rest].include?(type)
10
+ all: params.select { |t, _|
11
+ [:req, :opt, :rest].any? { |param_type| Bind.call(t, :==, param_type) }
10
12
  }.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]
13
+ required: params.select { |t, _| Bind.call(t, :==, :req) }.map { |_, n| n },
14
+ optional: params.select { |t, _| Bind.call(t, :==, :opt) }.map { |_, n| n },
15
+ rest: params.find { |t, _| Bind.call(t, :==, :rest) } & [1]
14
16
  ),
15
17
  positional_args: dry_call.args,
16
18
 
@@ -18,13 +20,13 @@ module Mocktail
18
20
  all: params.select { |type, _|
19
21
  [:keyreq, :key, :keyrest].include?(type)
20
22
  }.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]
23
+ required: params.select { |t, _| Bind.call(t, :==, :keyreq) }.map { |_, n| n },
24
+ optional: params.select { |t, _| Bind.call(t, :==, :key) }.map { |_, n| n },
25
+ rest: params.find { |t, _| Bind.call(t, :==, :keyrest) } & [1]
24
26
  ),
25
27
  keyword_args: dry_call.kwargs,
26
28
 
27
- block_param: params.find { |type, _| type == :block } & [1],
29
+ block_param: params.find { |t, _| Bind.call(t, :==, :block) } & [1],
28
30
  block_arg: dry_call.block
29
31
  )
30
32
  end
@@ -1,3 +1,5 @@
1
+ require_relative "../share/bind"
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
@@ -45,15 +47,22 @@ module Mocktail
45
47
  end
46
48
 
47
49
  def double_for_instance(thing)
48
- @doubles.find { |double| double.dry_instance == thing }
50
+ @doubles.find { |double|
51
+ # Intentionally calling directly to avoid an infinite recursion in Bind.call
52
+ Object.instance_method(:==).bind_call(double.dry_instance, thing)
53
+ }
49
54
  end
50
55
 
51
56
  def stubbings_for_double(double)
52
- @stubbings.select { |stubbing| stubbing.recording.double == double.dry_instance }
57
+ @stubbings.select { |stubbing|
58
+ Bind.call(stubbing.recording.double, :==, double.dry_instance)
59
+ }
53
60
  end
54
61
 
55
62
  def calls_for_double(double)
56
- @calls.select { |call| call.double == double.dry_instance }
63
+ @calls.select { |call|
64
+ Bind.call(call.double, :==, double.dry_instance)
65
+ }
57
66
  end
58
67
  end
59
68
  end
@@ -1,3 +1,3 @@
1
1
  module Mocktail
2
- VERSION = "1.1.0"
2
+ VERSION = "1.1.3"
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.1.0
4
+ version: 1.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Searls
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-01-27 00:00:00.000000000 Z
11
+ date: 2022-06-28 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
@@ -66,6 +67,7 @@ files:
66
67
  - lib/mocktail/replaces_type/redefines_new.rb
67
68
  - lib/mocktail/replaces_type/redefines_singleton_methods.rb
68
69
  - lib/mocktail/resets_state.rb
70
+ - lib/mocktail/share/bind.rb
69
71
  - lib/mocktail/share/cleans_backtrace.rb
70
72
  - lib/mocktail/share/creates_identifier.rb
71
73
  - lib/mocktail/share/determines_matching_calls.rb
@@ -117,7 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
117
119
  - !ruby/object:Gem::Version
118
120
  version: '0'
119
121
  requirements: []
120
- rubygems_version: 3.3.6
122
+ rubygems_version: 3.3.7
121
123
  signing_key:
122
124
  specification_version: 4
123
125
  summary: Take your objects, and make them a double