mocktail 1.1.3 → 1.2.1

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: 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