mocktail 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7eb64f90875e53a9bfd5b4d995f8526eb6695dd9bd8a88c850b02eed851cb96d
4
- data.tar.gz: ae2b4443a3740c375c70e7e490cbe3463d9672f17665ae47930b6ee614e9891d
3
+ metadata.gz: 0c303d4fb9ecc3a03e30fcfb52425cec2c4f3b2ca2ad3dadda3f9a09c1b8785a
4
+ data.tar.gz: 90b573a3b6417d1c084fa7ced22af80b0533e36c45240d42c9627b3bfde2a032
5
5
  SHA512:
6
- metadata.gz: de8d47071e93c4d406391a1155dea9b6a1a9ac89f56422ae29e6dda5f134c9e5e724ec8006aba833b5ff19d9d2dff4550384bc349506f5ab9cefe3fd2185af1d
7
- data.tar.gz: 5bf88d0aadc08fe9a9380312cf491146439e72e335091e2e79849196b74e93aba20b0fac2d122152a4661008ee243b5b3b39cb6408fc776b2cc2567ffca0924f
6
+ metadata.gz: f2aa7140202bfb3d5cadc45964f344cce19ba688d807b9c25b2033d27dd15710424fc00315633129429215892ea149160d2fbba2a8671bb2237867b12ce7c225
7
+ data.tar.gz: 95f1c02d8f23849f0a5932e8cddb3f811dc01962e13e0553f1964ebf9f77489b49ce375506e3f2a5f9132bcda7493b2ffc8a03dc61dd4f5018f84fe33995245f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ # 1.2.0
2
+
3
+ * Introduce the Mocktail.calls() API https://github.com/testdouble/mocktail/pull/16
4
+
5
+ # 1.1.3
6
+
7
+ * Improve the robustness of how we call the original methods on doubles &
8
+ replaced class methods on mocks internally by Mocktail to avoid behavior being
9
+ altered by how users configure mock objects
10
+
11
+ # 1.1.2
12
+
13
+ * Fix cases where classes that redefine built-in methods could cause issues when
14
+ Mocktail in turn called those methods internally
15
+ [#15](https://github.com/testdouble/mocktail/pull/15)
16
+
1
17
  # 1.1.1
2
18
 
3
19
  * Improve output for undefined singleton methods
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mocktail (1.1.2)
4
+ mocktail (1.2.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -12,7 +12,7 @@ and even safely override class methods.
12
12
 
13
13
  If you'd prefer a voice & video introduction to Mocktail aside from this README,
14
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)⚡️
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
16
  from RailsConf 2022.
17
17
 
18
18
  ## An aperitif
@@ -404,6 +404,10 @@ verify { |m| auditor.record!("ok", user_id: 1) { |block| block.call.is_a?(Date)
404
404
  * `ignore_block` will similarly allow the demonstration to forego specifying a
405
405
  block, even if the actual call receives one
406
406
 
407
+ Note that if you want to verify a method _wasn't_ called at all or called a
408
+ specific number of times—especially if you don't care about the parameters, you
409
+ may want to look at the [Mocktail.calls()](#mocktailcalls) API.
410
+
407
411
  ### Mocktail.matchers
408
412
 
409
413
  You'll probably never need to call `Mocktail.matchers` directly, because it's
@@ -807,6 +811,54 @@ The `reference` object will have details of the `call` itself, an array of
807
811
  `other_stubbings` defined on the faked method, and a `backtrace` to determine
808
812
  which call site produced the unexpected `nil` value.
809
813
 
814
+ ### Mocktail.calls
815
+
816
+ When practicing test-driven development, you may want to ensure that a
817
+ dependency wasn't called at all, and don't particularly care about the
818
+ parameters. To provide a terse way to express this, Mocktail offers a top-level
819
+ `calls(double, method_name = nil)` method that returns an array of the calls to
820
+ the mock (optionally filtered to a particular method name) in the order they
821
+ were called.
822
+
823
+ Suppose you were writing a test of this method for example:
824
+
825
+ ```ruby
826
+ def import_users
827
+ users_response = @gets_users.get
828
+ if users_response.success?
829
+ @upserts_users.upsert(users_response.data)
830
+ end
831
+ end
832
+ ```
833
+
834
+ A test case of the negative branch of that `if` statement (when `success?` is
835
+ false) might simply want to assert that `@upserts_users.upsert` wasn't called at
836
+ all, regardless of its parameters.
837
+
838
+ The easiest way to do this is to use `Mocktail.calls()` method, which is an
839
+ alias of [Mocktail.explain(double).reference.calls](#mocktailexplain) that can
840
+ filter to a specific method name. In the case of a test of the above method, you
841
+ could assert:
842
+
843
+ ```ruby
844
+ # Assert that the `upsert` method on the mock was never called
845
+ assert_equal 0, Mocktail.calls(@upserts_users, :upsert).size
846
+
847
+ # Assert that NO METHODS on the mock were called at all:
848
+ assert_equal 0, Mocktail.calls(@upserts_users).size
849
+ ```
850
+
851
+ If you're interested in doing more complicated introspection in the nature of
852
+ the calls, their ordering, and so forth, the `calls` method will return
853
+ `Mocktail::Call` values with the args, kwargs, block, and information about the
854
+ original class and method being mocked.
855
+
856
+ (While this behavior can technically be accomplished with `verify(times: 0) { …
857
+ }`, it's verbose and error prone to do so. Because `verify` is careful to only
858
+ assert exact argument matches, it can get pretty confusing to remember to tack
859
+ on `ignore_extra_args: true` and to call the method with zero args to cover all
860
+ cases.)
861
+
810
862
  ### Mocktail.reset
811
863
 
812
864
  This one's simple: you probably want to call `Mocktail.reset` after each test,
data/bin/rake ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rake' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rake", "rake")
@@ -0,0 +1,13 @@
1
+ module Mocktail
2
+ class CollectsCalls
3
+ def collect(double, method_name)
4
+ calls = ExplainsThing.new.explain(double).reference.calls
5
+
6
+ if method_name.nil?
7
+ calls
8
+ else
9
+ calls.select { |call| call.method.to_s == method_name.to_s }
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,19 +1,18 @@
1
1
  require_relative "../../share/cleans_backtrace"
2
- require_relative "../../share/compares_safely"
2
+ require_relative "../../share/bind"
3
3
 
4
4
  module Mocktail
5
5
  class DescribesUnsatisfiedStubbing
6
6
  def initialize
7
7
  @cleans_backtrace = CleansBacktrace.new
8
- @compares_safely = ComparesSafely.new
9
8
  end
10
9
 
11
10
  def describe(dry_call)
12
11
  UnsatisfyingCall.new(
13
12
  call: dry_call,
14
13
  other_stubbings: Mocktail.cabinet.stubbings.select { |stubbing|
15
- @compares_safely.compare(dry_call.double, stubbing.recording.double) &&
16
- @compares_safely.compare(dry_call.method, stubbing.recording.method)
14
+ Bind.call(dry_call.double, :==, stubbing.recording.double) &&
15
+ dry_call.method == stubbing.recording.method
17
16
  },
18
17
  backtrace: @cleans_backtrace.clean(Error.new).backtrace
19
18
  )
@@ -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,14 +1,10 @@
1
- require_relative "compares_safely"
1
+ require_relative "bind"
2
2
 
3
3
  module Mocktail
4
4
  class DeterminesMatchingCalls
5
- def initialize
6
- @compares_safely = ComparesSafely.new
7
- end
8
-
9
5
  def determine(real_call, demo_call, demo_config)
10
- @compares_safely.compare(real_call.double, demo_call.double) &&
11
- @compares_safely.compare(real_call.method, demo_call.method) &&
6
+ Bind.call(real_call.double, :==, demo_call.double) &&
7
+ real_call.method == demo_call.method &&
12
8
 
13
9
  # Matcher implementation will replace this:
14
10
  args_match?(real_call.args, demo_call.args, demo_config.ignore_extra_args) &&
@@ -55,11 +51,11 @@ module Mocktail
55
51
  end
56
52
 
57
53
  def match?(real_arg, demo_arg)
58
- if demo_arg.respond_to?(:is_mocktail_matcher?) &&
54
+ if Bind.call(demo_arg, :respond_to?, :is_mocktail_matcher?) &&
59
55
  demo_arg.is_mocktail_matcher?
60
56
  demo_arg.match?(real_arg)
61
57
  else
62
- demo_arg == real_arg # TODO <-- test if mock object and call safe compare if so, otherwise ==
58
+ Bind.call(demo_arg, :==, real_arg)
63
59
  end
64
60
  end
65
61
  end
@@ -1,22 +1,18 @@
1
- require_relative "../share/compares_safely"
1
+ require_relative "../share/bind"
2
2
 
3
3
  module Mocktail
4
4
  class TransformsParams
5
- def initialize
6
- @compares_safely = ComparesSafely.new
7
- end
8
-
9
5
  def transform(dry_call)
10
6
  params = dry_call.original_method.parameters
11
7
 
12
8
  Signature.new(
13
9
  positional_params: Params.new(
14
10
  all: params.select { |t, _|
15
- [:req, :opt, :rest].any? { |param_type| @compares_safely.compare(t, param_type) }
11
+ [:req, :opt, :rest].any? { |param_type| Bind.call(t, :==, param_type) }
16
12
  }.map { |_, name| name },
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]
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]
20
16
  ),
21
17
  positional_args: dry_call.args,
22
18
 
@@ -24,13 +20,13 @@ module Mocktail
24
20
  all: params.select { |type, _|
25
21
  [:keyreq, :key, :keyrest].include?(type)
26
22
  }.map { |_, name| name },
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]
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]
30
26
  ),
31
27
  keyword_args: dry_call.kwargs,
32
28
 
33
- block_param: params.find { |t, _| @compares_safely.compare(t, :block) } & [1],
29
+ block_param: params.find { |t, _| Bind.call(t, :==, :block) } & [1],
34
30
  block_arg: dry_call.block
35
31
  )
36
32
  end
@@ -1,4 +1,4 @@
1
- require_relative "../share/compares_safely"
1
+ require_relative "../share/bind"
2
2
 
3
3
  # The Cabinet stores all thread-local state, so anything that goes here
4
4
  # is guaranteed by Mocktail to be local to the currently-running thread
@@ -8,7 +8,6 @@ module Mocktail
8
8
  attr_reader :calls, :stubbings, :unsatisfying_calls
9
9
 
10
10
  def initialize
11
- @compares_safely = ComparesSafely.new
12
11
  @doubles = []
13
12
  @calls = []
14
13
  @stubbings = []
@@ -48,18 +47,21 @@ module Mocktail
48
47
  end
49
48
 
50
49
  def double_for_instance(thing)
51
- @doubles.find { |double| @compares_safely.compare(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
+ }
52
54
  end
53
55
 
54
56
  def stubbings_for_double(double)
55
57
  @stubbings.select { |stubbing|
56
- @compares_safely.compare(stubbing.recording.double, double.dry_instance)
58
+ Bind.call(stubbing.recording.double, :==, double.dry_instance)
57
59
  }
58
60
  end
59
61
 
60
62
  def calls_for_double(double)
61
63
  @calls.select { |call|
62
- @compares_safely.compare(call.double, double.dry_instance)
64
+ Bind.call(call.double, :==, double.dry_instance)
63
65
  }
64
66
  end
65
67
  end
@@ -1,3 +1,3 @@
1
1
  module Mocktail
2
- VERSION = "1.1.2"
2
+ VERSION = "1.2.0"
3
3
  end
data/lib/mocktail.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require_relative "mocktail/collects_calls"
1
2
  require_relative "mocktail/debug"
2
3
  require_relative "mocktail/dsl"
3
4
  require_relative "mocktail/errors"
@@ -67,6 +68,13 @@ module Mocktail
67
68
  ExplainsNils.new.explain
68
69
  end
69
70
 
71
+ # An alias for Mocktail.explain(double).reference.calls
72
+ # Takes an optional second parameter of the method name to filter only
73
+ # calls to that method
74
+ def self.calls(double, method_name = nil)
75
+ CollectsCalls.new.collect(double, method_name)
76
+ end
77
+
70
78
  # Stores most transactional state about calls & stubbing configurations
71
79
  # Anything returned by this is undocumented and could change at any time, so
72
80
  # don't commit code that relies on it!
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.2
4
+ version: 1.2.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: 2022-06-25 00:00:00.000000000 Z
11
+ date: 2022-09-21 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -27,8 +27,10 @@ files:
27
27
  - README.md
28
28
  - Rakefile
29
29
  - bin/console
30
+ - bin/rake
30
31
  - bin/setup
31
32
  - lib/mocktail.rb
33
+ - lib/mocktail/collects_calls.rb
32
34
  - lib/mocktail/debug.rb
33
35
  - lib/mocktail/dsl.rb
34
36
  - lib/mocktail/errors.rb
@@ -67,8 +69,8 @@ files:
67
69
  - lib/mocktail/replaces_type/redefines_new.rb
68
70
  - lib/mocktail/replaces_type/redefines_singleton_methods.rb
69
71
  - lib/mocktail/resets_state.rb
72
+ - lib/mocktail/share/bind.rb
70
73
  - lib/mocktail/share/cleans_backtrace.rb
71
- - lib/mocktail/share/compares_safely.rb
72
74
  - lib/mocktail/share/creates_identifier.rb
73
75
  - lib/mocktail/share/determines_matching_calls.rb
74
76
  - lib/mocktail/share/stringifies_call.rb
@@ -119,7 +121,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
119
121
  - !ruby/object:Gem::Version
120
122
  version: '0'
121
123
  requirements: []
122
- rubygems_version: 3.3.6
124
+ rubygems_version: 3.3.20
123
125
  signing_key:
124
126
  specification_version: 4
125
127
  summary: Take your objects, and make them a double
@@ -1,7 +0,0 @@
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