mock-suey 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +342 -4
- data/lib/.rbnext/3.0/mock_suey/core.rb +246 -0
- data/lib/.rbnext/3.0/mock_suey/ext/instance_class.rb +22 -0
- data/lib/.rbnext/3.0/mock_suey/ext/rspec.rb +37 -0
- data/lib/.rbnext/3.0/mock_suey/mock_contract.rb +150 -0
- data/lib/.rbnext/3.0/mock_suey/rspec/mock_context.rb +129 -0
- data/lib/.rbnext/3.0/mock_suey/type_checks/ruby.rb +251 -0
- data/lib/.rbnext/3.1/mock_suey/core.rb +246 -0
- data/lib/.rbnext/3.1/mock_suey/mock_contract.rb +150 -0
- data/lib/.rbnext/3.1/mock_suey/rspec/mock_context.rb +129 -0
- data/lib/.rbnext/3.1/mock_suey/rspec/proxy_method_invoked.rb +47 -0
- data/lib/.rbnext/3.1/mock_suey/tracer.rb +173 -0
- data/lib/.rbnext/3.1/mock_suey/type_checks/ruby.rb +251 -0
- data/lib/mock_suey/core.rb +246 -0
- data/lib/mock_suey/ext/instance_class.rb +22 -0
- data/lib/mock_suey/ext/rspec.rb +37 -0
- data/lib/mock_suey/logging.rb +29 -0
- data/lib/mock_suey/method_call.rb +71 -0
- data/lib/mock_suey/mock_contract.rb +150 -0
- data/lib/mock_suey/rspec/mock_context.rb +129 -0
- data/lib/mock_suey/rspec/proxy_method_invoked.rb +47 -0
- data/lib/mock_suey/rspec.rb +60 -0
- data/lib/mock_suey/tracer.rb +173 -0
- data/lib/mock_suey/type_checks/ruby.rb +251 -0
- data/lib/mock_suey/type_checks.rb +8 -0
- data/lib/mock_suey/version.rb +1 -1
- data/lib/mock_suey.rb +15 -0
- metadata +27 -3
@@ -0,0 +1,246 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MockSuey
|
4
|
+
class Configuration
|
5
|
+
# No freezing this const to allow third-party libraries
|
6
|
+
# to integrate with mock_suey
|
7
|
+
TYPE_CHECKERS = %w[ruby]
|
8
|
+
|
9
|
+
attr_accessor :debug,
|
10
|
+
:logger,
|
11
|
+
:log_level,
|
12
|
+
:color,
|
13
|
+
:store_mocked_calls,
|
14
|
+
:signature_load_dirs,
|
15
|
+
:raise_on_missing_types,
|
16
|
+
:raise_on_missing_auto_types,
|
17
|
+
:trace_real_calls,
|
18
|
+
:trace_real_calls_via
|
19
|
+
|
20
|
+
attr_reader :type_check, :auto_type_check, :verify_mock_contracts
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@debug = %w[1 y yes true t].include?(ENV["MOCK_SUEY_DEBUG"])
|
24
|
+
@log_level = debug ? :debug : :info
|
25
|
+
@color = nil
|
26
|
+
@store_mocked_calls = false
|
27
|
+
@type_check = nil
|
28
|
+
@signature_load_dirs = ["sig"]
|
29
|
+
@raise_on_missing_types = false
|
30
|
+
@raise_on_missing_auto_types = true
|
31
|
+
@trace_real_calls = false
|
32
|
+
@auto_type_check = false
|
33
|
+
@trace_real_calls_via = :prepend
|
34
|
+
end
|
35
|
+
|
36
|
+
def color?
|
37
|
+
return color unless color.nil?
|
38
|
+
|
39
|
+
logdev = logger.instance_variable_get(:@logdev)
|
40
|
+
return self.color = false unless logdev
|
41
|
+
|
42
|
+
output = logdev.instance_variable_get(:@dev)
|
43
|
+
return self.color = false unless output
|
44
|
+
|
45
|
+
self.color = output.is_a?(IO) && output.tty?
|
46
|
+
end
|
47
|
+
|
48
|
+
def type_check=(val)
|
49
|
+
if val.nil?
|
50
|
+
@type_check = nil
|
51
|
+
return
|
52
|
+
end
|
53
|
+
|
54
|
+
val = val.to_s
|
55
|
+
raise ArgumentError, "Unsupported type checker: #{val}. Supported: #{TYPE_CHECKERS.join(",")}" unless TYPE_CHECKERS.include?(val)
|
56
|
+
|
57
|
+
@type_check = val
|
58
|
+
end
|
59
|
+
|
60
|
+
def auto_type_check=(val)
|
61
|
+
if val
|
62
|
+
@trace_real_calls = true
|
63
|
+
@store_mocked_calls = true
|
64
|
+
@auto_type_check = true
|
65
|
+
else
|
66
|
+
@auto_type_check = val
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def verify_mock_contracts=(val)
|
71
|
+
if val
|
72
|
+
@trace_real_calls = true
|
73
|
+
@verify_mock_contracts = true
|
74
|
+
else
|
75
|
+
@verify_mock_contracts = val
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class << self
|
81
|
+
attr_reader :stored_mocked_calls, :tracer, :stored_real_calls
|
82
|
+
attr_accessor :type_checker
|
83
|
+
|
84
|
+
def config = @config ||= Configuration.new
|
85
|
+
|
86
|
+
def configure ; yield config; end
|
87
|
+
|
88
|
+
def logger = config.logger
|
89
|
+
|
90
|
+
def on_mocked_call(&block)
|
91
|
+
on_mocked_callbacks << block
|
92
|
+
end
|
93
|
+
|
94
|
+
def handle_mocked_call(call_obj)
|
95
|
+
on_mocked_callbacks.each { _1.call(call_obj) }
|
96
|
+
end
|
97
|
+
|
98
|
+
def on_mocked_callbacks
|
99
|
+
@on_mocked_callbacks ||= []
|
100
|
+
end
|
101
|
+
|
102
|
+
# Load extensions and start tracing if required
|
103
|
+
def cook
|
104
|
+
setup_logger
|
105
|
+
setup_type_checker
|
106
|
+
setup_mocked_calls_collection if config.store_mocked_calls
|
107
|
+
setup_real_calls_collection if config.trace_real_calls
|
108
|
+
end
|
109
|
+
|
110
|
+
# Run post-suite checks
|
111
|
+
def eat
|
112
|
+
@stored_real_calls = tracer.stop if config.trace_real_calls
|
113
|
+
|
114
|
+
offenses = []
|
115
|
+
|
116
|
+
if config.store_mocked_calls
|
117
|
+
logger.debug { "Stored mocked calls:\n#{stored_mocked_calls.map { " #{_1.inspect}" }.join("\n")}" }
|
118
|
+
end
|
119
|
+
|
120
|
+
if config.trace_real_calls
|
121
|
+
logger.debug { "Traced real calls:\n#{stored_real_calls.map { " #{_1.inspect}" }.join("\n")}" }
|
122
|
+
end
|
123
|
+
|
124
|
+
if config.auto_type_check
|
125
|
+
perform_auto_type_check(offenses)
|
126
|
+
end
|
127
|
+
|
128
|
+
if config.verify_mock_contracts
|
129
|
+
perform_contracts_verification(offenses)
|
130
|
+
end
|
131
|
+
|
132
|
+
offenses
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def setup_logger
|
138
|
+
if !config.logger || config.debug
|
139
|
+
config.logger = Logger.new($stdout)
|
140
|
+
config.logger.formatter = Logging::Formatter.new
|
141
|
+
end
|
142
|
+
config.logger.level = config.log_level
|
143
|
+
end
|
144
|
+
|
145
|
+
def setup_type_checker
|
146
|
+
return unless config.type_check
|
147
|
+
|
148
|
+
# Allow configuring type checher manually
|
149
|
+
unless type_checker
|
150
|
+
require "mock_suey/type_checks/#{config.type_check}"
|
151
|
+
const_name = config.type_check.split("_").map(&:capitalize).join
|
152
|
+
|
153
|
+
self.type_checker = MockSuey::TypeChecks.const_get(const_name)
|
154
|
+
.new(load_dirs: config.signature_load_dirs)
|
155
|
+
|
156
|
+
logger.info "Set up type checker: #{type_checker.class.name} (load_dirs: #{config.signature_load_dirs})"
|
157
|
+
end
|
158
|
+
|
159
|
+
raise_on_missing = config.raise_on_missing_types
|
160
|
+
|
161
|
+
on_mocked_call do |call_obj|
|
162
|
+
type_checker.typecheck!(call_obj, raise_on_missing: raise_on_missing)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def setup_mocked_calls_collection
|
167
|
+
logger.info "Collect mocked calls (MockSuey.stored_mocked_calls)"
|
168
|
+
|
169
|
+
@stored_mocked_calls = []
|
170
|
+
|
171
|
+
on_mocked_call { @stored_mocked_calls << _1 }
|
172
|
+
end
|
173
|
+
|
174
|
+
def setup_real_calls_collection
|
175
|
+
logger.info "Collect real calls via #{config.trace_real_calls_via} (MockSuey.stored_real_calls)"
|
176
|
+
|
177
|
+
@tracer = Tracer.new(via: config.trace_real_calls_via)
|
178
|
+
|
179
|
+
MockSuey::RSpec::MockContext.registry.each do |klass, methods|
|
180
|
+
logger.debug { "Trace #{klass} methods: #{methods.keys.join(", ")}" }
|
181
|
+
tracer.collect(klass, methods.keys)
|
182
|
+
end
|
183
|
+
|
184
|
+
tracer.start!
|
185
|
+
end
|
186
|
+
|
187
|
+
def perform_auto_type_check(offenses)
|
188
|
+
raise "No type checker configured" unless type_checker
|
189
|
+
|
190
|
+
# Generate signatures
|
191
|
+
type_checker.load_signatures_from_calls(stored_real_calls)
|
192
|
+
|
193
|
+
logger.info "Type-checking mocked calls against auto-generated signatures..."
|
194
|
+
|
195
|
+
was_offenses = offenses.size
|
196
|
+
|
197
|
+
# Verify stored mocked calls
|
198
|
+
raise_on_missing = config.raise_on_missing_auto_types
|
199
|
+
|
200
|
+
stored_mocked_calls.each do |call_obj|
|
201
|
+
type_checker.typecheck!(call_obj, raise_on_missing: raise_on_missing)
|
202
|
+
rescue RBS::Test::Tester::TypeError, TypeChecks::MissingSignature => err
|
203
|
+
call_obj.metadata[:error] = err
|
204
|
+
offenses << call_obj
|
205
|
+
end
|
206
|
+
|
207
|
+
failed_count = offenses.size - was_offenses
|
208
|
+
failed = failed_count > 0
|
209
|
+
|
210
|
+
if failed
|
211
|
+
logger.error "❌ Type-checking completed. Failed examples: #{failed_count}"
|
212
|
+
else
|
213
|
+
logger.info "✅ Type-checking completed. All good"
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def perform_contracts_verification(offenses)
|
218
|
+
logger.info "Verifying mock contracts..."
|
219
|
+
real_calls_per_class_method = stored_real_calls.group_by(&:receiver_class).tap do |grouped|
|
220
|
+
grouped.transform_values! { _1.group_by(&:method_name) }
|
221
|
+
end
|
222
|
+
|
223
|
+
was_offenses = offenses.size
|
224
|
+
|
225
|
+
MockSuey::RSpec::MockContext.registry.each do |klass, methods|
|
226
|
+
methods.values.flatten.each do |stub_call|
|
227
|
+
contract = MockContract.from_stub(stub_call)
|
228
|
+
logger.debug { "Generated contract:\n #{contract.inspect}\n (from stub: #{stub_call.inspect})" }
|
229
|
+
contract.verify!(real_calls_per_class_method.dig(klass, stub_call.method_name))
|
230
|
+
rescue MockContract::Error => err
|
231
|
+
stub_call.metadata[:error] = err
|
232
|
+
offenses << stub_call
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
failed_count = offenses.size - was_offenses
|
237
|
+
failed = failed_count > 0
|
238
|
+
|
239
|
+
if failed
|
240
|
+
logger.error "❌ Verifying mock contracts completed. Failed contracts: #{failed_count}"
|
241
|
+
else
|
242
|
+
logger.info "✅ Verifying mock contracts completed. All good"
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "mock_suey/ext/instance_class"
|
4
|
+
|
5
|
+
module MockSuey
|
6
|
+
class MockContract
|
7
|
+
using Ext::InstanceClass
|
8
|
+
|
9
|
+
class Error < StandardError
|
10
|
+
attr_reader :contract
|
11
|
+
|
12
|
+
def initialize(contract, msg)
|
13
|
+
@contract = contract
|
14
|
+
super(msg)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def captured_calls_message(calls)
|
20
|
+
calls.map do |call|
|
21
|
+
contract.args_pattern.map.with_index do |arg, i|
|
22
|
+
(ANYTHING == arg) ? "_" : call.arguments[i].inspect
|
23
|
+
end.join(", ").then do |args_desc|
|
24
|
+
" (#{args_desc}) -> #{call.return_value.class}"
|
25
|
+
end
|
26
|
+
end.uniq.join("\n")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class NoMethodCalls < Error
|
31
|
+
def initialize(contract)
|
32
|
+
super(
|
33
|
+
contract,
|
34
|
+
"Mock contract verification failed:\n" \
|
35
|
+
" No method calls captured for #{contract.method_desc}"
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class NoMatchingMethodCalls < Error
|
41
|
+
def initialize(contract, real_calls)
|
42
|
+
@contract = contract
|
43
|
+
super(
|
44
|
+
contract,
|
45
|
+
"Mock contract verification failed:\n" \
|
46
|
+
" No matching calls captured for #{contract.inspect}.\n" \
|
47
|
+
" Captured call patterns:\n" \
|
48
|
+
"#{captured_calls_message(real_calls)}"
|
49
|
+
)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class NoMatchingReturnType < Error
|
54
|
+
def initialize(contract, real_calls)
|
55
|
+
@contract = contract
|
56
|
+
super(
|
57
|
+
contract,
|
58
|
+
"Mock contract verification failed:\n" \
|
59
|
+
" No calls with the expected return type captured for #{contract.inspect}.\n" \
|
60
|
+
" Captured call patterns:\n" \
|
61
|
+
"#{captured_calls_message(real_calls)}"
|
62
|
+
)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
ANYTHING = Object.new.freeze
|
67
|
+
|
68
|
+
def self.from_stub(call_obj)
|
69
|
+
call_obj => {receiver_class:, method_name:, return_value:}
|
70
|
+
|
71
|
+
args_pattern = call_obj.arguments.map do
|
72
|
+
contractable_arg?(_1) ? _1 : ANYTHING
|
73
|
+
end
|
74
|
+
|
75
|
+
new(
|
76
|
+
receiver_class: receiver_class,
|
77
|
+
method_name: method_name,
|
78
|
+
args_pattern: args_pattern,
|
79
|
+
return_type: return_value.class
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.contractable_arg?(val)
|
84
|
+
case val
|
85
|
+
when TrueClass, FalseClass, Numeric, String, Regexp, NilClass
|
86
|
+
true
|
87
|
+
when Array
|
88
|
+
val.all? { |v| contractable_arg?(v) }
|
89
|
+
when Hash
|
90
|
+
contractable_arg?(val.values)
|
91
|
+
else
|
92
|
+
false
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
attr_reader :receiver_class, :method_name,
|
97
|
+
:args_pattern, :return_type
|
98
|
+
|
99
|
+
def initialize(receiver_class:, method_name:, args_pattern:, return_type:)
|
100
|
+
@receiver_class = receiver_class
|
101
|
+
@method_name = method_name
|
102
|
+
@args_pattern = args_pattern
|
103
|
+
@return_type = return_type
|
104
|
+
end
|
105
|
+
|
106
|
+
def verify!(calls)
|
107
|
+
return if noop?
|
108
|
+
|
109
|
+
raise NoMethodCalls.new(self) if calls.nil? || calls.empty?
|
110
|
+
|
111
|
+
matching_input_calls = calls.select { matching_args?(_1) }
|
112
|
+
raise NoMatchingMethodCalls.new(self, calls) if matching_input_calls.empty?
|
113
|
+
|
114
|
+
matching_input_calls.each do
|
115
|
+
return if _1.return_value.class <= return_type
|
116
|
+
end
|
117
|
+
|
118
|
+
raise NoMatchingReturnType.new(self, matching_input_calls)
|
119
|
+
end
|
120
|
+
|
121
|
+
def noop? = args_pattern.all? { _1 == ANYTHING }
|
122
|
+
|
123
|
+
def method_desc
|
124
|
+
delimeter = receiver_class.singleton_class? ? "." : "#"
|
125
|
+
|
126
|
+
"#{receiver_class.instance_class_name}#{delimeter}#{method_name}"
|
127
|
+
end
|
128
|
+
|
129
|
+
def inspect
|
130
|
+
args_pattern.map do
|
131
|
+
(_1 == ANYTHING) ? "_" : _1.inspect
|
132
|
+
end.join(", ").then do |args_desc|
|
133
|
+
"#{method_desc}: (#{args_desc}) -> #{return_type}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def matching_args?(call)
|
140
|
+
args_pattern.each.with_index do |arg, i|
|
141
|
+
next if arg == ANYTHING
|
142
|
+
# Use case-eq here to make it possible to use composed
|
143
|
+
# matchers in the future
|
144
|
+
return false unless arg === call.arguments[i]
|
145
|
+
end
|
146
|
+
|
147
|
+
true
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "mock_suey/ext/rspec"
|
4
|
+
require "mock_suey/ext/instance_class"
|
5
|
+
|
6
|
+
module MockSuey
|
7
|
+
module RSpec
|
8
|
+
# Special type of shared context for mocks.
|
9
|
+
# The main difference is that it can track mocked classes and methods.
|
10
|
+
module MockContext
|
11
|
+
NAMESPACE = "mock::"
|
12
|
+
|
13
|
+
class << self
|
14
|
+
attr_writer :context_namespace
|
15
|
+
|
16
|
+
def context_namespace = @context_namespace || NAMESPACE
|
17
|
+
|
18
|
+
def collector = @collector ||= MocksCollector.new
|
19
|
+
|
20
|
+
def registry = collector.registry
|
21
|
+
end
|
22
|
+
|
23
|
+
class MocksCollector
|
24
|
+
using Ext::RSpec
|
25
|
+
using Ext::InstanceClass
|
26
|
+
|
27
|
+
# Registry contains all identified mocks/stubs in a form:
|
28
|
+
# Hash[Class: Hash[Symbol method_name, Array[MethodCall]]
|
29
|
+
attr_reader :registry
|
30
|
+
|
31
|
+
def initialize
|
32
|
+
@registry = Hash.new { |h, k| h[k] = {} }
|
33
|
+
@mocks = {}
|
34
|
+
end
|
35
|
+
|
36
|
+
def watch(context_id)
|
37
|
+
return if mocks.key?(context_id)
|
38
|
+
|
39
|
+
mocks[context_id] = true
|
40
|
+
|
41
|
+
evaluate_context!(context_id)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
attr_reader :mocks
|
47
|
+
|
48
|
+
def evaluate_context!(context_id)
|
49
|
+
store = registry
|
50
|
+
|
51
|
+
Class.new(::RSpec::Core::ExampleGroup) do
|
52
|
+
def self.metadata = {}
|
53
|
+
|
54
|
+
def self.filtered_examples = examples
|
55
|
+
|
56
|
+
::RSpec::Core::MemoizedHelpers.define_helpers_on(self)
|
57
|
+
|
58
|
+
include_context(context_id)
|
59
|
+
|
60
|
+
specify("true") { expect(true).to be(true) }
|
61
|
+
|
62
|
+
after do
|
63
|
+
::RSpec::Mocks.space.proxies.values.each do |proxy|
|
64
|
+
proxy.method_doubles.values.each do |double|
|
65
|
+
method_name = double.method_name
|
66
|
+
receiver_class = proxy.target_class
|
67
|
+
|
68
|
+
# Simple doubles don't have targets
|
69
|
+
next unless receiver_class
|
70
|
+
|
71
|
+
# TODO: Make conversion customizable (see proxy_method_invoked)
|
72
|
+
if method_name == :new && receiver_class.singleton_class?
|
73
|
+
receiver_class, method_name = receiver_class.instance_class, :initialize
|
74
|
+
end
|
75
|
+
|
76
|
+
expected_calls = store[receiver_class][method_name] = []
|
77
|
+
|
78
|
+
double.stubs.each do |stub|
|
79
|
+
arguments =
|
80
|
+
if stub.expected_args in [::RSpec::Mocks::ArgumentMatchers::NoArgsMatcher]
|
81
|
+
[]
|
82
|
+
else
|
83
|
+
stub.expected_args
|
84
|
+
end
|
85
|
+
|
86
|
+
return_value = stub.implementation.terminal_action.call
|
87
|
+
|
88
|
+
expected_calls << MethodCall.new(
|
89
|
+
receiver_class: receiver_class,
|
90
|
+
method_name: method_name,
|
91
|
+
arguments: arguments,
|
92
|
+
return_value: return_value
|
93
|
+
)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end.run
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
module DSL
|
103
|
+
def mock_context(name, **opts, &block)
|
104
|
+
::RSpec.shared_context("#{MockContext.context_namespace}#{name}", **opts, &block)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
module ExampleGroup
|
109
|
+
def include_mock_context(name)
|
110
|
+
context_id = "#{MockContext.context_namespace}#{name}"
|
111
|
+
|
112
|
+
MockContext.collector.watch(context_id)
|
113
|
+
include_context(context_id)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Extending RSpec
|
121
|
+
RSpec.extend(MockSuey::RSpec::MockContext::DSL)
|
122
|
+
|
123
|
+
if RSpec.configuration.expose_dsl_globally?
|
124
|
+
Object.include(MockSuey::RSpec::MockContext::DSL)
|
125
|
+
Module.extend(MockSuey::RSpec::MockContext::DSL)
|
126
|
+
end
|
127
|
+
|
128
|
+
RSpec::Core::ExampleGroup.extend(MockSuey::RSpec::MockContext::ExampleGroup)
|
129
|
+
RSpec::Core::ExampleGroup.extend(MockSuey::RSpec::MockContext::DSL)
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
RSpec::Mocks::VerifyingMethodDouble
|
5
|
+
rescue LoadError
|
6
|
+
warn "Couldn't find VerifyingMethodDouble class from rspec-mocks"
|
7
|
+
return
|
8
|
+
end
|
9
|
+
|
10
|
+
require "mock_suey/ext/rspec"
|
11
|
+
require "mock_suey/ext/instance_class"
|
12
|
+
|
13
|
+
module MockSuey
|
14
|
+
module RSpec
|
15
|
+
using Ext::RSpec
|
16
|
+
using Ext::InstanceClass
|
17
|
+
|
18
|
+
module ProxyMethodInvokedHook
|
19
|
+
def proxy_method_invoked(obj, *args, &block)
|
20
|
+
return super if obj.is_a?(::RSpec::Mocks::TestDouble) && !obj.is_a?(::RSpec::Mocks::VerifyingDouble)
|
21
|
+
|
22
|
+
receiver_class = @proxy.target_class
|
23
|
+
method_name = @method_name
|
24
|
+
|
25
|
+
# TODO: Make conversion customizable to support .perform_later -> #perform
|
26
|
+
# and other similar use-cases
|
27
|
+
if method_name == :new && receiver_class.singleton_class?
|
28
|
+
receiver_class, method_name = receiver_class.instance_class, :initialize
|
29
|
+
end
|
30
|
+
|
31
|
+
method_call = MockSuey::MethodCall.new(
|
32
|
+
receiver_class: receiver_class,
|
33
|
+
method_name: method_name,
|
34
|
+
arguments: args,
|
35
|
+
metadata: {example: ::RSpec.current_example}
|
36
|
+
)
|
37
|
+
|
38
|
+
super.tap do |ret|
|
39
|
+
method_call.return_value = ret
|
40
|
+
MockSuey.handle_mocked_call(method_call)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
RSpec::Mocks::MethodDouble.prepend(MockSuey::RSpec::ProxyMethodInvokedHook)
|