mock-suey 0.0.1 → 0.1.0
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 +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)
|