mocktail 0.0.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 +7 -0
- data/.github/workflows/main.yml +18 -0
- data/.gitignore +8 -0
- data/.standard.yml +1 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +62 -0
- data/LICENSE.txt +20 -0
- data/README.md +557 -0
- data/Rakefile +11 -0
- data/bin/console +35 -0
- data/bin/setup +8 -0
- data/lib/mocktail/dsl.rb +21 -0
- data/lib/mocktail/errors.rb +15 -0
- data/lib/mocktail/handles_dry_call/fulfills_stubbing/finds_satisfaction.rb +16 -0
- data/lib/mocktail/handles_dry_call/fulfills_stubbing.rb +21 -0
- data/lib/mocktail/handles_dry_call/logs_call.rb +7 -0
- data/lib/mocktail/handles_dry_call/validates_arguments.rb +57 -0
- data/lib/mocktail/handles_dry_call.rb +19 -0
- data/lib/mocktail/handles_dry_new_call.rb +36 -0
- data/lib/mocktail/imitates_type/ensures_imitation_support.rb +11 -0
- data/lib/mocktail/imitates_type/makes_double/declares_dry_class.rb +95 -0
- data/lib/mocktail/imitates_type/makes_double.rb +18 -0
- data/lib/mocktail/imitates_type.rb +19 -0
- data/lib/mocktail/initializes_mocktail.rb +17 -0
- data/lib/mocktail/matcher_presentation.rb +15 -0
- data/lib/mocktail/matchers/any.rb +18 -0
- data/lib/mocktail/matchers/base.rb +25 -0
- data/lib/mocktail/matchers/captor.rb +52 -0
- data/lib/mocktail/matchers/includes.rb +24 -0
- data/lib/mocktail/matchers/is_a.rb +11 -0
- data/lib/mocktail/matchers/matches.rb +13 -0
- data/lib/mocktail/matchers/not.rb +11 -0
- data/lib/mocktail/matchers/numeric.rb +18 -0
- data/lib/mocktail/matchers/that.rb +24 -0
- data/lib/mocktail/matchers.rb +14 -0
- data/lib/mocktail/records_demonstration.rb +32 -0
- data/lib/mocktail/registers_matcher.rb +52 -0
- data/lib/mocktail/registers_stubbing.rb +19 -0
- data/lib/mocktail/replaces_next.rb +36 -0
- data/lib/mocktail/replaces_type/redefines_new.rb +26 -0
- data/lib/mocktail/replaces_type/redefines_singleton_methods.rb +39 -0
- data/lib/mocktail/replaces_type.rb +26 -0
- data/lib/mocktail/resets_state.rb +9 -0
- data/lib/mocktail/share/determines_matching_calls.rb +60 -0
- data/lib/mocktail/share/simulates_argument_error.rb +28 -0
- data/lib/mocktail/value/cabinet.rb +41 -0
- data/lib/mocktail/value/call.rb +15 -0
- data/lib/mocktail/value/demo_config.rb +10 -0
- data/lib/mocktail/value/double.rb +11 -0
- data/lib/mocktail/value/matcher_registry.rb +19 -0
- data/lib/mocktail/value/stubbing.rb +24 -0
- data/lib/mocktail/value/top_shelf.rb +61 -0
- data/lib/mocktail/value/type_replacement.rb +11 -0
- data/lib/mocktail/value.rb +8 -0
- data/lib/mocktail/verifies_call/finds_verifiable_calls.rb +15 -0
- data/lib/mocktail/verifies_call/raises_verification_error/gathers_calls_of_method.rb +10 -0
- data/lib/mocktail/verifies_call/raises_verification_error/stringifies_call.rb +47 -0
- data/lib/mocktail/verifies_call/raises_verification_error.rb +63 -0
- data/lib/mocktail/verifies_call.rb +29 -0
- data/lib/mocktail/version.rb +3 -0
- data/lib/mocktail.rb +63 -0
- data/mocktail.gemspec +31 -0
- metadata +107 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "bundler/setup"
|
3
|
+
require "mocktail"
|
4
|
+
|
5
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
6
|
+
# with your gem easier. You can also use a different console, if you like.
|
7
|
+
class Negroni
|
8
|
+
def self.ingredients
|
9
|
+
[:gin, :campari, :sweet_vermouth]
|
10
|
+
end
|
11
|
+
|
12
|
+
def shake!(shaker)
|
13
|
+
shaker.mix(self.class.ingredients)
|
14
|
+
end
|
15
|
+
|
16
|
+
def sip(amount)
|
17
|
+
raise "unimplemented"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
include Mocktail::DSL
|
22
|
+
class UserRepository
|
23
|
+
def find(id); end
|
24
|
+
|
25
|
+
def transaction(&blk); end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Auditor
|
29
|
+
def record!(message, user:, action: nil); end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
34
|
+
require "pry"
|
35
|
+
Pry.start
|
data/bin/setup
ADDED
data/lib/mocktail/dsl.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Mocktail
|
2
|
+
module DSL
|
3
|
+
def stubs(ignore_block: false, ignore_extra_args: false, ignore_arity: false, times: nil, &demo)
|
4
|
+
RegistersStubbing.new.register(demo, DemoConfig.new(
|
5
|
+
ignore_block: ignore_block,
|
6
|
+
ignore_extra_args: ignore_extra_args,
|
7
|
+
ignore_arity: ignore_arity,
|
8
|
+
times: times
|
9
|
+
))
|
10
|
+
end
|
11
|
+
|
12
|
+
def verify(ignore_block: false, ignore_extra_args: false, ignore_arity: false, times: nil, &demo)
|
13
|
+
VerifiesCall.new.verify(demo, DemoConfig.new(
|
14
|
+
ignore_block: ignore_block,
|
15
|
+
ignore_extra_args: ignore_extra_args,
|
16
|
+
ignore_arity: ignore_arity,
|
17
|
+
times: times
|
18
|
+
))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Mocktail
|
2
|
+
class Error < StandardError; end
|
3
|
+
|
4
|
+
class UnexpectedError < Error; end
|
5
|
+
|
6
|
+
class UnsupportedMocktail < Error; end
|
7
|
+
|
8
|
+
class MissingDemonstrationError < Error; end
|
9
|
+
|
10
|
+
class AmbiguousDemonstrationError < Error; end
|
11
|
+
|
12
|
+
class InvalidMatcherError < Error; end
|
13
|
+
|
14
|
+
class VerificationError < Error; end
|
15
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative "../../share/determines_matching_calls"
|
2
|
+
|
3
|
+
module Mocktail
|
4
|
+
class FindsSatisfaction
|
5
|
+
def initialize
|
6
|
+
@determines_matching_calls = DeterminesMatchingCalls.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def find(dry_call)
|
10
|
+
Mocktail.cabinet.stubbings.reverse.find { |stubbing|
|
11
|
+
@determines_matching_calls.determine(dry_call, stubbing.recording, stubbing.demo_config) &&
|
12
|
+
(stubbing.demo_config.times.nil? || stubbing.demo_config.times > stubbing.satisfaction_count)
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative "fulfills_stubbing/finds_satisfaction"
|
2
|
+
|
3
|
+
module Mocktail
|
4
|
+
class FulfillsStubbing
|
5
|
+
def initialize
|
6
|
+
@finds_satisfaction = FindsSatisfaction.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def fulfill(dry_call)
|
10
|
+
if (stubbing = satisfaction(dry_call))
|
11
|
+
stubbing.satisfied!
|
12
|
+
stubbing.effect&.call(dry_call)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def satisfaction(dry_call)
|
17
|
+
return if Mocktail.cabinet.demonstration_in_progress?
|
18
|
+
@finds_satisfaction.find(dry_call)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require_relative "../share/simulates_argument_error"
|
2
|
+
|
3
|
+
module Mocktail
|
4
|
+
class ValidatesArguments
|
5
|
+
def self.disable!
|
6
|
+
Thread.current[:mocktail_arity_validation_disabled] = true
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.enable!
|
10
|
+
Thread.current[:mocktail_arity_validation_disabled] = false
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.disabled?
|
14
|
+
Thread.current[:mocktail_arity_validation_disabled]
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.optional(disable, &blk)
|
18
|
+
return blk.call unless disable
|
19
|
+
|
20
|
+
disable!
|
21
|
+
blk.call.tap do
|
22
|
+
enable!
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
@simulates_argument_error = SimulatesArgumentError.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def validate(dry_call)
|
31
|
+
return if self.class.disabled?
|
32
|
+
|
33
|
+
arg_params, kwarg_params = dry_call.original_method.parameters.reject { |type, _|
|
34
|
+
type == :block
|
35
|
+
}.partition { |type, _|
|
36
|
+
[:req, :opt, :rest].include?(type)
|
37
|
+
}
|
38
|
+
|
39
|
+
unless args_match?(arg_params, dry_call.args) &&
|
40
|
+
kwargs_match?(kwarg_params, dry_call.kwargs)
|
41
|
+
raise @simulates_argument_error.simulate(arg_params, dry_call.args, kwarg_params, dry_call.kwargs)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def args_match?(arg_params, args)
|
48
|
+
args.size >= arg_params.count { |type, _| type == :req } &&
|
49
|
+
(arg_params.any? { |type, _| type == :rest } || args.size <= arg_params.size)
|
50
|
+
end
|
51
|
+
|
52
|
+
def kwargs_match?(kwarg_params, kwargs)
|
53
|
+
kwarg_params.select { |type, _| type == :keyreq }.all? { |_, name| kwargs.key?(name) } &&
|
54
|
+
(kwarg_params.any? { |type, _| type == :keyrest } || kwargs.keys.all? { |name| kwarg_params.any? { |_, key| name == key } })
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative "handles_dry_call/fulfills_stubbing"
|
2
|
+
require_relative "handles_dry_call/logs_call"
|
3
|
+
require_relative "handles_dry_call/validates_arguments"
|
4
|
+
|
5
|
+
module Mocktail
|
6
|
+
class HandlesDryCall
|
7
|
+
def initialize
|
8
|
+
@validates_arguments = ValidatesArguments.new
|
9
|
+
@logs_call = LogsCall.new
|
10
|
+
@fulfills_stubbing = FulfillsStubbing.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def handle(dry_call)
|
14
|
+
@validates_arguments.validate(dry_call)
|
15
|
+
@logs_call.log(dry_call)
|
16
|
+
@fulfills_stubbing.fulfill(dry_call)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Mocktail
|
2
|
+
class HandlesDryNewCall
|
3
|
+
def initialize
|
4
|
+
@validates_arguments = ValidatesArguments.new
|
5
|
+
@logs_call = LogsCall.new
|
6
|
+
@fulfills_stubbing = FulfillsStubbing.new
|
7
|
+
@imitates_type = ImitatesType.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def handle(type, args, kwargs, block)
|
11
|
+
@validates_arguments.validate(Call.new(
|
12
|
+
original_method: type.instance_method(:initialize),
|
13
|
+
args: args,
|
14
|
+
kwargs: kwargs,
|
15
|
+
block: block
|
16
|
+
))
|
17
|
+
|
18
|
+
new_call = Call.new(
|
19
|
+
singleton: true,
|
20
|
+
double: type,
|
21
|
+
original_type: type,
|
22
|
+
dry_type: type,
|
23
|
+
method: :new,
|
24
|
+
args: args,
|
25
|
+
kwargs: kwargs,
|
26
|
+
block: block
|
27
|
+
)
|
28
|
+
@logs_call.log(new_call)
|
29
|
+
if @fulfills_stubbing.satisfaction(new_call)
|
30
|
+
@fulfills_stubbing.fulfill(new_call)
|
31
|
+
else
|
32
|
+
@imitates_type.imitate(type)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Mocktail
|
2
|
+
class EnsuresImitationSupport
|
3
|
+
def ensure(type)
|
4
|
+
unless type.is_a?(Class) || type.is_a?(Module)
|
5
|
+
raise UnsupportedMocktail.new <<~MSG.tr("\n", " ")
|
6
|
+
Mocktail.of() can only mix mocktail instances of modules and classes.
|
7
|
+
MSG
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Mocktail
|
2
|
+
class DeclaresDryClass
|
3
|
+
def initialize
|
4
|
+
@handles_dry_call = HandlesDryCall.new
|
5
|
+
end
|
6
|
+
|
7
|
+
def declare(type)
|
8
|
+
type_type = type_of(type)
|
9
|
+
instance_methods = instance_methods_on(type)
|
10
|
+
dry_class = Class.new(Object) {
|
11
|
+
include type if type_type == :module
|
12
|
+
|
13
|
+
def initialize(*args, **kwargs, &blk)
|
14
|
+
end
|
15
|
+
|
16
|
+
define_method :is_a?, ->(thing) {
|
17
|
+
type.ancestors.include?(thing)
|
18
|
+
}
|
19
|
+
alias_method :kind_of?, :is_a?
|
20
|
+
|
21
|
+
if type_type == :class
|
22
|
+
define_method :instance_of?, ->(thing) {
|
23
|
+
type == thing
|
24
|
+
}
|
25
|
+
end
|
26
|
+
}
|
27
|
+
|
28
|
+
add_stringify_methods!(dry_class, :to_s, type, type_type, instance_methods)
|
29
|
+
add_stringify_methods!(dry_class, :inspect, type, type_type, instance_methods)
|
30
|
+
|
31
|
+
define_double_methods!(dry_class, type, instance_methods)
|
32
|
+
|
33
|
+
dry_class
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def define_double_methods!(dry_class, type, instance_methods)
|
39
|
+
handles_dry_call = @handles_dry_call
|
40
|
+
instance_methods.each do |method|
|
41
|
+
dry_class.define_method method, ->(*args, **kwargs, &block) {
|
42
|
+
handles_dry_call.handle(Call.new(
|
43
|
+
singleton: false,
|
44
|
+
double: self,
|
45
|
+
original_type: type,
|
46
|
+
dry_type: self.class,
|
47
|
+
method: method,
|
48
|
+
original_method: type.instance_method(method),
|
49
|
+
args: args,
|
50
|
+
kwargs: kwargs,
|
51
|
+
block: block
|
52
|
+
))
|
53
|
+
}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def add_stringify_methods!(dry_class, method_name, type, type_type, instance_methods)
|
58
|
+
dry_class.define_singleton_method method_name, -> {
|
59
|
+
if (id_matches = super().match(/:([0-9a-fx]+)>$/))
|
60
|
+
"#<Class #{"including module " if type.instance_of?(Module)}for mocktail of #{type.name}:#{id_matches[1]}>"
|
61
|
+
else
|
62
|
+
super()
|
63
|
+
end
|
64
|
+
}
|
65
|
+
|
66
|
+
unless instance_methods.include?(method_name)
|
67
|
+
dry_class.define_method method_name, -> {
|
68
|
+
if (id_matches = super().match(/:([0-9a-fx]+)>$/))
|
69
|
+
"#<Mocktail of #{type.name}:#{id_matches[1]}>"
|
70
|
+
else
|
71
|
+
super()
|
72
|
+
end
|
73
|
+
}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def type_of(type)
|
78
|
+
if type.is_a?(Class)
|
79
|
+
:class
|
80
|
+
elsif type.is_a?(Module)
|
81
|
+
:module
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def instance_methods_on(type)
|
86
|
+
type.instance_methods.reject { |m|
|
87
|
+
ignored_ancestors.include?(type.instance_method(m).owner)
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
def ignored_ancestors
|
92
|
+
Object.ancestors
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative "makes_double/declares_dry_class"
|
2
|
+
|
3
|
+
module Mocktail
|
4
|
+
class MakesDouble
|
5
|
+
def initialize
|
6
|
+
@declares_dry_class = DeclaresDryClass.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def make(klass)
|
10
|
+
dry_type = @declares_dry_class.declare(klass)
|
11
|
+
Double.new(
|
12
|
+
original_type: klass,
|
13
|
+
dry_type: dry_type,
|
14
|
+
dry_instance: dry_type.new
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative "imitates_type/ensures_imitation_support"
|
2
|
+
require_relative "imitates_type/makes_double"
|
3
|
+
|
4
|
+
module Mocktail
|
5
|
+
class ImitatesType
|
6
|
+
def initialize
|
7
|
+
@top_shelf = TopShelf.instance
|
8
|
+
@ensures_imitation_support = EnsuresImitationSupport.new
|
9
|
+
@makes_double = MakesDouble.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def imitate(type)
|
13
|
+
@ensures_imitation_support.ensure(type)
|
14
|
+
@makes_double.make(type).tap do |double|
|
15
|
+
Mocktail.cabinet.store_double(double)
|
16
|
+
end.dry_instance
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Mocktail
|
2
|
+
class InitializesMocktail
|
3
|
+
def init
|
4
|
+
[
|
5
|
+
Mocktail::Matchers::Any,
|
6
|
+
Mocktail::Matchers::Includes,
|
7
|
+
Mocktail::Matchers::IsA,
|
8
|
+
Mocktail::Matchers::Matches,
|
9
|
+
Mocktail::Matchers::Not,
|
10
|
+
Mocktail::Matchers::Numeric,
|
11
|
+
Mocktail::Matchers::That
|
12
|
+
].each do |matcher_type|
|
13
|
+
Mocktail.register_matcher(matcher_type)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Mocktail
|
2
|
+
class MatcherPresentation
|
3
|
+
def respond_to_missing?(name, include_private = false)
|
4
|
+
!!MatcherRegistry.instance.get(name) || super
|
5
|
+
end
|
6
|
+
|
7
|
+
def method_missing(name, *args, **kwargs, &blk)
|
8
|
+
if (matcher = MatcherRegistry.instance.get(name))
|
9
|
+
matcher.new(*args, **kwargs, &blk)
|
10
|
+
else
|
11
|
+
super
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Mocktail::Matchers
|
2
|
+
class Base
|
3
|
+
# Custom matchers can receive any args, kwargs, or block they want. Usually
|
4
|
+
# single-argument, though, so that's defaulted here and in #insepct
|
5
|
+
def initialize(expected)
|
6
|
+
@expected = expected
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.matcher_name
|
10
|
+
raise Mocktail::Error.new("The `matcher_name` class method must return a valid method name")
|
11
|
+
end
|
12
|
+
|
13
|
+
def match?(actual)
|
14
|
+
raise Mocktail::Error.new("Matchers must implement `match?(argument)`")
|
15
|
+
end
|
16
|
+
|
17
|
+
def inspect
|
18
|
+
"#{self.class.matcher_name}(#{@expected.inspect})"
|
19
|
+
end
|
20
|
+
|
21
|
+
def is_mocktail_matcher?
|
22
|
+
true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Mocktail::Matchers
|
2
|
+
# Captors are conceptually complex implementations, but with a simple usage/purpose:
|
3
|
+
# They are values the user can create and hold onto that will return a matcher
|
4
|
+
# and then "capture" the value made by the real call, for later analysis & assertion.
|
5
|
+
#
|
6
|
+
# Unlike other matchers, these don't make any useful sense for stubbing, but are
|
7
|
+
# very useful when asserting complication call verifications
|
8
|
+
#
|
9
|
+
# The fact the user will need the reference outside the verification call is
|
10
|
+
# why this is a top-level method on Mocktail, and not included in the |m| block
|
11
|
+
# arg to stubs/verify
|
12
|
+
#
|
13
|
+
# See Mockito, which is the earliest implementation I know of:
|
14
|
+
# https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Captor.html
|
15
|
+
class Captor
|
16
|
+
class Capture < Mocktail::Matchers::Base
|
17
|
+
def self.matcher_name
|
18
|
+
:capture
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :value
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
@value = nil
|
25
|
+
@captured = false
|
26
|
+
end
|
27
|
+
|
28
|
+
def match?(actual)
|
29
|
+
@value = actual
|
30
|
+
@captured = true
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
def captured?
|
35
|
+
@captured
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_reader :capture
|
40
|
+
def initialize
|
41
|
+
@capture = Capture.new
|
42
|
+
end
|
43
|
+
|
44
|
+
def captured?
|
45
|
+
@capture.captured?
|
46
|
+
end
|
47
|
+
|
48
|
+
def value
|
49
|
+
@capture.value
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Mocktail::Matchers
|
2
|
+
class Includes < Base
|
3
|
+
def self.matcher_name
|
4
|
+
:includes
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(*expecteds)
|
8
|
+
@expecteds = expecteds
|
9
|
+
end
|
10
|
+
|
11
|
+
def match?(actual)
|
12
|
+
@expecteds.all? { |expected|
|
13
|
+
(actual.respond_to?(:include?) && actual.include?(expected)) ||
|
14
|
+
(actual.is_a?(Hash) && expected.is_a?(Hash) && expected.all? { |k, v| actual[k] == v })
|
15
|
+
}
|
16
|
+
rescue
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
def inspect
|
21
|
+
"#{self.class.matcher_name}(#{@expecteds.map(&:inspect).join(", ")})"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Mocktail::Matchers
|
2
|
+
class Numeric < Base
|
3
|
+
def self.matcher_name
|
4
|
+
:numeric
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
end
|
9
|
+
|
10
|
+
def match?(actual)
|
11
|
+
[Integer, Float, (BigDecimal if defined?(BigDecimal))].include?(actual.class)
|
12
|
+
end
|
13
|
+
|
14
|
+
def inspect
|
15
|
+
"numeric"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Mocktail::Matchers
|
2
|
+
class That < Base
|
3
|
+
def self.matcher_name
|
4
|
+
:that
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(&blk)
|
8
|
+
if blk.nil?
|
9
|
+
raise "The `that` matcher must be passed a block (e.g. `that { |arg| … }`)"
|
10
|
+
end
|
11
|
+
@blk = blk
|
12
|
+
end
|
13
|
+
|
14
|
+
def match?(actual)
|
15
|
+
@blk.call(actual)
|
16
|
+
rescue
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
def inspect
|
21
|
+
"that {…}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Mocktail
|
2
|
+
module Matchers
|
3
|
+
end
|
4
|
+
end
|
5
|
+
|
6
|
+
require_relative "matchers/base"
|
7
|
+
require_relative "matchers/any"
|
8
|
+
require_relative "matchers/captor"
|
9
|
+
require_relative "matchers/includes"
|
10
|
+
require_relative "matchers/is_a"
|
11
|
+
require_relative "matchers/matches"
|
12
|
+
require_relative "matchers/not"
|
13
|
+
require_relative "matchers/numeric"
|
14
|
+
require_relative "matchers/that"
|