mocktail 1.1.0 → 1.1.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|