mocktail 1.1.0 → 1.1.3
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 +4 -4
- data/CHANGELOG.md +18 -0
- data/Gemfile.lock +13 -13
- data/README.md +31 -2
- data/lib/mocktail/debug.rb +49 -0
- data/lib/mocktail/handles_dry_call/fulfills_stubbing/describes_unsatisfied_stubbing.rb +2 -1
- data/lib/mocktail/imitates_type/makes_double/declares_dry_class.rb +3 -0
- data/lib/mocktail/matchers/any.rb +3 -1
- data/lib/mocktail/matchers/numeric.rb +3 -1
- data/lib/mocktail/raises_neato_no_method_error.rb +1 -1
- data/lib/mocktail/replaces_type/redefines_singleton_methods.rb +23 -0
- data/lib/mocktail/share/bind.rb +14 -0
- data/lib/mocktail/share/determines_matching_calls.rb +5 -3
- data/lib/mocktail/simulates_argument_error/transforms_params.rb +11 -9
- data/lib/mocktail/value/cabinet.rb +12 -3
- data/lib/mocktail/version.rb +1 -1
- data/lib/mocktail.rb +1 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0aa187046d2e07280352ba6f0296f5573bda910acb111e06d0932f0e2518064e
|
4
|
+
data.tar.gz: c3e5e74d586969ea5dd88dc86c20a9d2e6f223f1bb8a438325583a8d8fb20bae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
15
|
-
parser (3.1.
|
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.
|
22
|
+
regexp_parser (2.3.0)
|
23
23
|
rexml (3.2.5)
|
24
|
-
rubocop (1.
|
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.
|
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.
|
34
|
-
parser (>= 3.
|
35
|
-
rubocop-performance (1.13.
|
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.
|
45
|
-
standard (1.
|
46
|
-
rubocop (= 1.
|
47
|
-
rubocop-performance (= 1.13.
|
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://
|
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
|
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
|
-
|
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
|
-
|
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)
|
@@ -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
|
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
|
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
|
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 { |
|
9
|
-
[:req, :opt, :rest].
|
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
|
12
|
-
optional: params.select { |t, _| t
|
13
|
-
rest: params.find { |
|
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
|
22
|
-
optional: params.select { |t, _| t
|
23
|
-
rest: params.find { |
|
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 { |
|
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|
|
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|
|
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|
|
63
|
+
@calls.select { |call|
|
64
|
+
Bind.call(call.double, :==, double.dry_instance)
|
65
|
+
}
|
57
66
|
end
|
58
67
|
end
|
59
68
|
end
|
data/lib/mocktail/version.rb
CHANGED
data/lib/mocktail.rb
CHANGED
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.
|
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-
|
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.
|
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
|