mocktail 1.1.3 → 1.2.1

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: 0aa187046d2e07280352ba6f0296f5573bda910acb111e06d0932f0e2518064e
4
- data.tar.gz: c3e5e74d586969ea5dd88dc86c20a9d2e6f223f1bb8a438325583a8d8fb20bae
3
+ metadata.gz: f98c176c091bd92d1c3f28be3c8c3eea66cbc3f3b56f9a2fac3f7f78d5eb2775
4
+ data.tar.gz: 9422af274277a791b225540f74e3c77f99aff75b908fb1529c56d6f47e35a27f
5
5
  SHA512:
6
- metadata.gz: 224a377d46c11b830b90a828e6a0c294e940c3492cde42fda9aa0f865b97cdc69773571d4464bd4bbe185aa29e6860c5a4c2b3b87a458382a84b330ddce5df3e
7
- data.tar.gz: 10562536842265afc7f6bef9fa63a8ebab657e78941f13bc0e38cb49cf8f43785d1d926d0f5afe103165d337fe2163cba88aa4aee220004c8a2494e5f5493d4c
6
+ metadata.gz: bc5ae9538c17efdc5675c1f19ca1daaa72dea52162f469fd4a295e68d6d5755792f8533a4c7244d0146e82edfb688dab3a608ea449ba57da3b56ac2e15c75f48
7
+ data.tar.gz: 381e52bf6c28151c0ab1dfba3f300df8cd8bc5cf218ddcc735e7dfdced93649f5f4e299fffefc449eb24cf3589c4067d23f7cffb313262c54409290ca10ac1a2
data/.standard.yml CHANGED
@@ -1 +1 @@
1
- ruby_version: 2.7
1
+ ruby_version: 3.0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ # 1.2.1
2
+
3
+ * Adds support for faking methods that use options hashes that are called with
4
+ and without curly braces [#17](https://github.com/testdouble/mocktail/pull/17)
5
+ (This is a sweeping change and there will probably be bugs.)
6
+
7
+ # 1.2.0
8
+
9
+ * Introduce the Mocktail.calls() API https://github.com/testdouble/mocktail/pull/16
10
+
1
11
  # 1.1.3
2
12
 
3
13
  * Improve the robustness of how we call the original methods on doubles &
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mocktail (1.1.3)
4
+ mocktail (1.2.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -9,30 +9,32 @@ GEM
9
9
  ast (2.4.2)
10
10
  coderay (1.1.3)
11
11
  docile (1.4.0)
12
+ json (2.6.2)
12
13
  method_source (1.0.0)
13
- minitest (5.15.0)
14
+ minitest (5.16.3)
14
15
  parallel (1.22.1)
15
- parser (3.1.2.0)
16
+ parser (3.1.2.1)
16
17
  ast (~> 2.4.1)
17
18
  pry (0.14.1)
18
19
  coderay (~> 1.1)
19
20
  method_source (~> 1.0)
20
21
  rainbow (3.1.1)
21
22
  rake (13.0.6)
22
- regexp_parser (2.3.0)
23
+ regexp_parser (2.6.0)
23
24
  rexml (3.2.5)
24
- rubocop (1.27.0)
25
+ rubocop (1.39.0)
26
+ json (~> 2.3)
25
27
  parallel (~> 1.10)
26
- parser (>= 3.1.0.0)
28
+ parser (>= 3.1.2.1)
27
29
  rainbow (>= 2.2.2, < 4.0)
28
30
  regexp_parser (>= 1.8, < 3.0)
29
- rexml
30
- rubocop-ast (>= 1.16.0, < 2.0)
31
+ rexml (>= 3.2.5, < 4.0)
32
+ rubocop-ast (>= 1.23.0, < 2.0)
31
33
  ruby-progressbar (~> 1.7)
32
34
  unicode-display_width (>= 1.4.0, < 3.0)
33
- rubocop-ast (1.17.0)
35
+ rubocop-ast (1.23.0)
34
36
  parser (>= 3.1.1.0)
35
- rubocop-performance (1.13.3)
37
+ rubocop-performance (1.15.0)
36
38
  rubocop (>= 1.7.0, < 2.0)
37
39
  rubocop-ast (>= 0.4.0)
38
40
  ruby-progressbar (1.11.0)
@@ -42,10 +44,10 @@ GEM
42
44
  simplecov_json_formatter (~> 0.1)
43
45
  simplecov-html (0.12.3)
44
46
  simplecov_json_formatter (0.1.4)
45
- standard (1.10.0)
46
- rubocop (= 1.27.0)
47
- rubocop-performance (= 1.13.3)
48
- unicode-display_width (2.1.0)
47
+ standard (1.18.0)
48
+ rubocop (= 1.39.0)
49
+ rubocop-performance (= 1.15.0)
50
+ unicode-display_width (2.3.0)
49
51
 
50
52
  PLATFORMS
51
53
  arm64-darwin-20
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,53 @@ 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. To provide a terse way to express this,
818
+ Mocktail offers a top-level `calls(double, method_name = nil)` method that
819
+ returns an array of the calls to the mock (optionally filtered to a
820
+ particular method name) in the order they were called.
821
+
822
+ Suppose you were writing a test of this method for example:
823
+
824
+ ```ruby
825
+ def import_users
826
+ users_response = @gets_users.get
827
+ if users_response.success?
828
+ @upserts_users.upsert(users_response.data)
829
+ end
830
+ end
831
+ ```
832
+
833
+ A test case of the negative branch of that `if` statement (when `success?` is
834
+ false) might simply want to assert that `@upserts_users.upsert` wasn't called at
835
+ all, regardless of its parameters.
836
+
837
+ The easiest way to do this is to use `Mocktail.calls()` method, which is an
838
+ alias of [Mocktail.explain(double).reference.calls](#mocktailexplain) that can
839
+ filter to a specific method name. In the case of a test of the above method, you
840
+ could assert:
841
+
842
+ ```ruby
843
+ # Assert that the `upsert` method on the mock was never called
844
+ assert_equal 0, Mocktail.calls(@upserts_users, :upsert).size
845
+
846
+ # Assert that NO METHODS on the mock were called at all:
847
+ assert_equal 0, Mocktail.calls(@upserts_users).size
848
+ ```
849
+
850
+ If you're interested in doing more complicated introspection in the nature of
851
+ the calls, their ordering, and so forth, the `calls` method will return
852
+ `Mocktail::Call` values with the args, kwargs, block, and information about the
853
+ original class and method being mocked.
854
+
855
+ (While this behavior can technically be accomplished with `verify(times: 0) { …
856
+ }`, it's verbose and error prone to do so. Because `verify` is careful to only
857
+ assert exact argument matches, it can get pretty confusing to remember to tack
858
+ on `ignore_extra_args: true` and to call the method with zero args to cover all
859
+ cases.)
860
+
810
861
  ### Mocktail.reset
811
862
 
812
863
  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
@@ -0,0 +1,47 @@
1
+ module Mocktail
2
+ class ReconstructsCall
3
+ def reconstruct(double:, call_binding:, default_args:, dry_class:, type:, method:, original_method:, signature:)
4
+ Call.new(
5
+ singleton: false,
6
+ double: double,
7
+ original_type: type,
8
+ dry_type: dry_class,
9
+ method: method,
10
+ original_method: original_method,
11
+ args: args_for(signature, call_binding, default_args),
12
+ kwargs: kwargs_for(signature, call_binding, default_args),
13
+ block: call_binding.local_variable_get(signature.block_param || ::Mocktail::Signature::DEFAULT_BLOCK_PARAM)
14
+ )
15
+ end
16
+
17
+ private
18
+
19
+ def args_for(signature, call_binding, default_args)
20
+ arg_names, rest_name = non_default_args(signature.positional_params, default_args)
21
+
22
+ arg_values = arg_names.map { |p| call_binding.local_variable_get(p) }
23
+ rest_value = call_binding.local_variable_get(rest_name) if rest_name
24
+
25
+ arg_values + (rest_value || [])
26
+ end
27
+
28
+ def kwargs_for(signature, call_binding, default_args)
29
+ kwarg_names, kwrest_name = non_default_args(signature.keyword_params, default_args)
30
+
31
+ kwarg_values = kwarg_names.to_h { |p| [p, call_binding.local_variable_get(p)] }
32
+ kwrest_value = call_binding.local_variable_get(kwrest_name) if kwrest_name
33
+
34
+ kwarg_values.merge(kwrest_value || {})
35
+ end
36
+
37
+ def non_default_args(params, default_args)
38
+ named_args = params.allowed
39
+ .reject { |p| default_args&.key?(p) }
40
+ rest_arg = if params.rest && !default_args&.key?(params.rest)
41
+ params.rest
42
+ end
43
+
44
+ [named_args, rest_arg]
45
+ end
46
+ end
47
+ end
@@ -1,8 +1,11 @@
1
+ require_relative "declares_dry_class/reconstructs_call"
2
+
1
3
  module Mocktail
2
4
  class DeclaresDryClass
3
5
  def initialize
4
- @handles_dry_call = HandlesDryCall.new
5
6
  @raises_neato_no_method_error = RaisesNeatoNoMethodError.new
7
+ @transforms_params = TransformsParams.new
8
+ @stringifies_method_signature = StringifiesMethodSignature.new
6
9
  end
7
10
 
8
11
  def declare(type, instance_methods)
@@ -39,24 +42,31 @@ module Mocktail
39
42
  private
40
43
 
41
44
  def define_double_methods!(dry_class, type, instance_methods)
42
- handles_dry_call = @handles_dry_call
43
45
  instance_methods.each do |method|
44
46
  dry_class.undef_method(method) if dry_class.method_defined?(method)
45
-
46
- dry_class.define_method method, ->(*args, **kwargs, &block) {
47
- Debug.guard_against_mocktail_accidentally_calling_mocks_if_debugging!
48
- handles_dry_call.handle(Call.new(
49
- singleton: false,
50
- double: self,
51
- original_type: type,
52
- dry_type: dry_class,
53
- method: method,
54
- original_method: type.instance_method(method),
55
- args: args,
56
- kwargs: kwargs,
57
- block: block
58
- ))
47
+ parameters = type.instance_method(method).parameters
48
+ signature = @transforms_params.transform(Call.new, params: parameters)
49
+ method_signature = @stringifies_method_signature.stringify(signature)
50
+ __mocktail_closure = {
51
+ dry_class: dry_class,
52
+ type: type,
53
+ method: method,
54
+ original_method: type.instance_method(method),
55
+ signature: signature
59
56
  }
57
+
58
+ dry_class.define_method method,
59
+ eval(<<-RUBBY, binding, __FILE__, __LINE__ + 1) # standard:disable Security/Eval
60
+ ->#{method_signature} do
61
+ ::Mocktail::Debug.guard_against_mocktail_accidentally_calling_mocks_if_debugging!
62
+ ::Mocktail::HandlesDryCall.new.handle(::Mocktail::ReconstructsCall.new.reconstruct(
63
+ double: self,
64
+ call_binding: __send__(:binding),
65
+ default_args: (__send__(:binding).local_variable_defined?(:__mocktail_default_args) ? __send__(:binding).local_variable_get(:__mocktail_default_args) : {}),
66
+ **__mocktail_closure
67
+ ))
68
+ end
69
+ RUBBY
60
70
  end
61
71
  end
62
72
 
@@ -71,7 +71,7 @@ module Mocktail
71
71
 
72
72
  <<~MSG
73
73
 
74
- There #{corrections.size == 1 ? "is" : "are"} also #{corrections.size} similar method#{"s" if corrections.size != 1} on #{call.original_type.name}.
74
+ There #{(corrections.size == 1) ? "is" : "are"} also #{corrections.size} similar method#{"s" if corrections.size != 1} on #{call.original_type.name}.
75
75
 
76
76
  Did you mean?
77
77
  #{corrections.map { |c| " #{c}" }.join("\n")}
@@ -30,7 +30,7 @@ module Mocktail
30
30
  }
31
31
  end
32
32
 
33
- mocktails.size == 1 ? mocktails.first : mocktails
33
+ (mocktails.size == 1) ? mocktails.first : mocktails
34
34
  end
35
35
  end
36
36
  end
@@ -15,6 +15,7 @@ module Mocktail
15
15
  private
16
16
 
17
17
  def args_match?(real_args, demo_args, ignore_extra_args)
18
+ # Guard clause for performance:
18
19
  return true if ignore_extra_args && demo_args.empty?
19
20
 
20
21
  (
@@ -2,9 +2,7 @@ require_relative "../share/bind"
2
2
 
3
3
  module Mocktail
4
4
  class TransformsParams
5
- def transform(dry_call)
6
- params = dry_call.original_method.parameters
7
-
5
+ def transform(dry_call, params: dry_call.original_method.parameters)
8
6
  Signature.new(
9
7
  positional_params: Params.new(
10
8
  all: params.select { |t, _|
@@ -12,7 +10,7 @@ module Mocktail
12
10
  }.map { |_, name| name },
13
11
  required: params.select { |t, _| Bind.call(t, :==, :req) }.map { |_, n| n },
14
12
  optional: params.select { |t, _| Bind.call(t, :==, :opt) }.map { |_, n| n },
15
- rest: params.find { |t, _| Bind.call(t, :==, :rest) } & [1]
13
+ rest: params.find { |t, _| Bind.call(t, :==, :rest) }&.last
16
14
  ),
17
15
  positional_args: dry_call.args,
18
16
 
@@ -22,11 +20,11 @@ module Mocktail
22
20
  }.map { |_, name| name },
23
21
  required: params.select { |t, _| Bind.call(t, :==, :keyreq) }.map { |_, n| n },
24
22
  optional: params.select { |t, _| Bind.call(t, :==, :key) }.map { |_, n| n },
25
- rest: params.find { |t, _| Bind.call(t, :==, :keyrest) } & [1]
23
+ rest: params.find { |t, _| Bind.call(t, :==, :keyrest) }&.last
26
24
  ),
27
25
  keyword_args: dry_call.kwargs,
28
26
 
29
- block_param: params.find { |t, _| Bind.call(t, :==, :block) } & [1],
27
+ block_param: params.find { |t, _| Bind.call(t, :==, :block) }&.last,
30
28
  block_arg: dry_call.block
31
29
  )
32
30
  end
@@ -0,0 +1,45 @@
1
+ module Mocktail
2
+ class StringifiesMethodSignature
3
+ def stringify(signature)
4
+ positional_params = positional(signature)
5
+ keyword_params = keyword(signature)
6
+ block_param = block(signature)
7
+
8
+ "(#{[positional_params, keyword_params, block_param].compact.join(", ")})"
9
+ end
10
+
11
+ private
12
+
13
+ def positional(signature)
14
+ params = signature.positional_params.all.map do |name|
15
+ if signature.positional_params.allowed.include?(name)
16
+ "#{name} = ((__mocktail_default_args ||= {})[:#{name}] = nil)"
17
+ elsif signature.positional_params.rest == name
18
+ "*#{(name == :*) ? Signature::DEFAULT_REST_ARGS : name}"
19
+ end
20
+ end.compact
21
+
22
+ params.join(", ") if params.any?
23
+ end
24
+
25
+ def keyword(signature)
26
+ params = signature.keyword_params.all.map do |name|
27
+ if signature.keyword_params.allowed.include?(name)
28
+ "#{name}: ((__mocktail_default_args ||= {})[:#{name}] = nil)"
29
+ elsif signature.keyword_params.rest == name
30
+ "**#{(name == :**) ? Signature::DEFAULT_REST_KWARGS : name}"
31
+ end
32
+ end.compact
33
+
34
+ params.join(", ") if params.any?
35
+ end
36
+
37
+ def block(signature)
38
+ if signature.block_param && signature.block_param != :&
39
+ "&#{signature.block_param}"
40
+ else
41
+ "&#{Signature::DEFAULT_BLOCK_PARAM}"
42
+ end
43
+ end
44
+ end
45
+ end
@@ -8,6 +8,9 @@ module Mocktail
8
8
  :block_arg,
9
9
  keyword_init: true
10
10
  )
11
+ DEFAULT_REST_ARGS = "args"
12
+ DEFAULT_REST_KWARGS = "kwargs"
13
+ DEFAULT_BLOCK_PARAM = "blk"
11
14
  end
12
15
 
13
16
  class Params < Struct.new(
@@ -26,7 +29,7 @@ module Mocktail
26
29
  end
27
30
 
28
31
  def allowed
29
- required + optional
32
+ all.select { |name| required.include?(name) || optional.include?(name) }
30
33
  end
31
34
 
32
35
  def rest?
@@ -1,3 +1,3 @@
1
1
  module Mocktail
2
- VERSION = "1.1.3"
2
+ VERSION = "1.2.1"
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"
@@ -16,6 +17,7 @@ require_relative "mocktail/replaces_next"
16
17
  require_relative "mocktail/replaces_type"
17
18
  require_relative "mocktail/resets_state"
18
19
  require_relative "mocktail/simulates_argument_error"
20
+ require_relative "mocktail/stringifies_method_signature"
19
21
  require_relative "mocktail/value"
20
22
  require_relative "mocktail/verifies_call"
21
23
  require_relative "mocktail/version"
@@ -67,6 +69,13 @@ module Mocktail
67
69
  ExplainsNils.new.explain
68
70
  end
69
71
 
72
+ # An alias for Mocktail.explain(double).reference.calls
73
+ # Takes an optional second parameter of the method name to filter only
74
+ # calls to that method
75
+ def self.calls(double, method_name = nil)
76
+ CollectsCalls.new.collect(double, method_name)
77
+ end
78
+
70
79
  # Stores most transactional state about calls & stubbing configurations
71
80
  # Anything returned by this is undocumented and could change at any time, so
72
81
  # 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.3
4
+ version: 1.2.1
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-28 00:00:00.000000000 Z
11
+ date: 2022-11-17 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
@@ -45,6 +47,7 @@ files:
45
47
  - lib/mocktail/imitates_type/ensures_imitation_support.rb
46
48
  - lib/mocktail/imitates_type/makes_double.rb
47
49
  - lib/mocktail/imitates_type/makes_double/declares_dry_class.rb
50
+ - lib/mocktail/imitates_type/makes_double/declares_dry_class/reconstructs_call.rb
48
51
  - lib/mocktail/imitates_type/makes_double/gathers_fakeable_instance_methods.rb
49
52
  - lib/mocktail/initializes_mocktail.rb
50
53
  - lib/mocktail/matcher_presentation.rb
@@ -77,6 +80,7 @@ files:
77
80
  - lib/mocktail/simulates_argument_error/reconciles_args_with_params.rb
78
81
  - lib/mocktail/simulates_argument_error/recreates_message.rb
79
82
  - lib/mocktail/simulates_argument_error/transforms_params.rb
83
+ - lib/mocktail/stringifies_method_signature.rb
80
84
  - lib/mocktail/value.rb
81
85
  - lib/mocktail/value/cabinet.rb
82
86
  - lib/mocktail/value/call.rb
@@ -119,7 +123,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
119
123
  - !ruby/object:Gem::Version
120
124
  version: '0'
121
125
  requirements: []
122
- rubygems_version: 3.3.7
126
+ rubygems_version: 3.3.20
123
127
  signing_key:
124
128
  specification_version: 4
125
129
  summary: Take your objects, and make them a double