mocktail 1.1.2 → 1.2.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: 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