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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b136abffc56541079a6df6b63167f4cd465283fe16e096749d18f5656adb4174
4
- data.tar.gz: 9d76f5c3c086350b3c78750ecb2f0aabba698dc9899c4514ffb529e0e2d23be4
3
+ metadata.gz: 4631d070cd8722c45646c5a06453dd9eb80f34cd18dde0e0d6a5bfe0f085299f
4
+ data.tar.gz: 0a353b089256d5641fdfa426b4fd5922a06a6ca05e44fb196dbece77e9f58b62
5
5
  SHA512:
6
- metadata.gz: 20e94c8af89c30a2a21038b2979063355bd1b6378bde3c66e6d6f0b9ae44d812b9c46d37a690dc98b8ac4ff942cf6891b7f30fd8520bab9de9beb62fbc735340
7
- data.tar.gz: f756ba2ecdb919c253e7e482f54b1b7cfee0fb00098d870d2e8a4233725bf03500580c0d5f730b3e8ff6ed4c69347459f328b8bae1ade17f9b6fd8d1000a8512
6
+ metadata.gz: f676d7ebbdab9bc1874e2cda0633ba566868a8bcfc4c8319dda3bb662331a05539790e6324b105715893feaf9b071f37aa1cc27e12484f4eeea7dd0138b93efc
7
+ data.tar.gz: c7840271c1a2588100496034b06e4b0b5d1b5d62f6b36fc0813113aa8f0232a3c475ee5aa52cd1ebaad67123d36bb39ed64cfc13e5ce4805b0d9f8ae8ce824b1
data/CHANGELOG.md CHANGED
@@ -2,4 +2,8 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.1.0 (2022-11-25) 🎈
6
+
7
+ - Initial release.
8
+
5
9
  [@palkan]: https://github.com/palkan
data/README.md CHANGED
@@ -2,12 +2,27 @@
2
2
 
3
3
  # Mock Suey
4
4
 
5
- _Coming soon_
5
+ <img align="right" height="168" width="120"
6
+ title="Mock Suey logo" src="./assets/logo.png">
6
7
 
7
- Utilities to keep mocks in line with real objects.
8
+ A collection of tools to keep mocks in line with real objects.
8
9
 
9
10
  > Based on the RubyConf 2022 talk ["Weaving and seaming mocks"][the-talk]
10
11
 
12
+ ## Table of contents
13
+
14
+ - [Installation](#installation)
15
+ - [Typed doubles](#typed-doubles)
16
+ - [Using with RBS](#using-with-rbs)
17
+ - [Typed doubles limitations](#typed-doubles-limitations)
18
+ - [Mock context](#mock-context)
19
+ - [Auto-generated type signatures and post-run checks](#auto-generated-type-signatures-and-post-run-checks)
20
+ - [Mock contracts verification](#mock-contracts-verification)
21
+ - [Tracking stubbed method calls](#tracking-stubbed-method-calls)
22
+ - [Tracking real method calls](#tracking-real-method-calls)
23
+ - [Configuration](#configuration)
24
+ - [Future development](#future-development)
25
+
11
26
  ## Installation
12
27
 
13
28
  ```ruby
@@ -17,9 +32,328 @@ group :test do
17
32
  end
18
33
  ```
19
34
 
20
- ## Usage
35
+ Then, drop `"require 'mock-suey'` to your `spec_helper.rb` / `rails_helper.rb` / `whatever_helper.rb`.
36
+
37
+ ## Typed doubles
38
+
39
+ MockSuey enhances verified doubles by adding type-checking support: every mocked method call is checked against the corresponding method signature (if present), and an exception is raised if types mismatch.
40
+
41
+ Consider an example:
42
+
43
+ ```ruby
44
+ let(:array_double) { instance_double("Array") }
45
+
46
+ specify "#take" do
47
+ allow(array_double).to receve(:take).and_return([1, 2, 3])
48
+
49
+ expect(array_double.take("three")).to eq([1, 2, 3])
50
+ end
51
+ ```
52
+
53
+ This test passes with plain RSpec, because from the verified double perspective everything is valid. However, calling `[].take("string")` raises a TypeError in runtime.
54
+
55
+ With MockSuey and [RBS][rbs], we can make verified doubles stricter and ensure that the types we use in method stubs are correct.
56
+
57
+ To enable typed verified doubles, you must explicitly configure a type checker.
58
+
59
+ ### Using with RBS
60
+
61
+ To use MockSuey with RBS, configure it as follows:
62
+
63
+ ```ruby
64
+ MockSuey.configure do |config|
65
+ config.type_check = :ruby
66
+ # Optional: specify signature directries to use ("sig" is used by default)
67
+ # config.signature_load_dirs = ["sig"]
68
+ # Optional: specify whether to raise an exception if no signature found
69
+ # config.raise_on_missing_types = false
70
+ end
71
+ ```
72
+
73
+ Make sure that `rbs` gem is present in the bundle (MockSuey doesn't require it as a runtime dependency).
74
+
75
+ That's it! Now all mocked methods are type-checked.
76
+
77
+ ### Typed doubles limitations
78
+
79
+ Typed doubles rely on the type signatures being defined. What if you don't have types (or don't want to add them)? There are two options:
80
+
81
+ 1) Adding type signatures only for the objects being mocked. You don't even need to type check your code or cover it with types. Instead, you can rely on runtime checks made in tests for real objects and use typed doubles for mocked objects.
82
+
83
+ 2) Auto-generating types on-the-fly from the real call traces (see below).
84
+
85
+ ## Mock context
86
+
87
+ Mock context is a re-usable mocking/stubbing configuration. Keeping a _library of mocks_
88
+ helps to keep fake objects under control. The idea is similar to data fixtures (and heavily inspired by the [fixturama][] gem).
89
+
90
+ Technically, mock contexts are _shared contexts_ (in RSpec sense) that _know_ which objects and methods are being mocked at the boot time, not at the run time. We use this knowledge to collect calls made on _real_ objects (so we can use them for the mocked calls verification later).
91
+
92
+ ### Defining and including mock contexts
93
+
94
+ The API is similar to shared contexts:
95
+
96
+ ```ruby
97
+ # Define a context
98
+ RSpec.mock_context "Anyway::Env" do
99
+ let(:testo_env) do
100
+ {
101
+ "a" => "x",
102
+ "data" => {
103
+ "key" => "value"
104
+ }
105
+ }
106
+ end
107
+
108
+ before do
109
+ env_double = instance_double("Anyway::Env")
110
+ allow(::Anyway::Env).to receive(:new).and_return(env_double)
111
+
112
+ allow(env_double).to receive(:fetch).with("UNKNOWN", any_args).and_return(Anyway::Env::Parsed.new({}, nil))
113
+ allow(env_double).to receive(:fetch).with("TESTO", any_args).and_return(Anyway::Env::Parsed.new(testo_env, nil))
114
+ allow(env_double).to receive(:fetch).with("", any_args).and_return(nil)
115
+ end
116
+ end
117
+
118
+ # Include in a test file
119
+ describe Anyway::Loaders::Env do
120
+ include_mock_context "Anyway::Env"
121
+
122
+ # ...
123
+ end
124
+ ```
125
+
126
+ It's recommended to keep mock contexts under `spec/mocks` or `spec/fixtures/mocks` and load them in the RSpec configuration file:
127
+
128
+ ```ruby
129
+ Dir["#{__dir__}/mocks/**/*.rb"].sort.each { |f| require f }
130
+ ```
131
+
132
+ ### Accessing mocked objects information
133
+
134
+ You can get the registry of mocked objects and methods after all tests were loaded,
135
+ for example, in the `before(:suite)` hook:
136
+
137
+ ```ruby
138
+ RSpec.configure do |config|
139
+ config.before(:suite) do
140
+ MockSuey::RSpec::MockContext.registry.each do |klass, methods|
141
+ methods.each do |method_name, stub_calls|
142
+ # Stub calls is a method call object,
143
+ # containing the information about the stubbed call:
144
+ # - receiver_class == klass
145
+ # - method_name == method_name
146
+ # - arguments: expected arguments (empty if expects no arguments)
147
+ # - return_value: stubbed return value
148
+ end
149
+ end
150
+ end
151
+ end
152
+ ```
153
+
154
+ ## Auto-generated type signatures and post-run checks
155
+
156
+ We can combine typed doubles and mock contexts to provide type-checking capabilities to codebase not using type signatures. For that, we can generate type signatures automatically by tracing _real_ object calls.
157
+
158
+ You must opt-in to use this feature:
159
+
160
+ ```ruby
161
+ MockSuey.configure do |config|
162
+ # Make sure type checker is configured
163
+ config.type_check = :ruby
164
+ config.auto_type_check = true
165
+ # Choose the real objects tracing method
166
+ config.trace_real_calls_via = :prepend # or :trace_point
167
+ # Whether to raise if type is missing in a post-check
168
+ # (i.e., hasn't been generated)
169
+ config.raise_on_missing_auto_types = true
170
+ end
171
+ ```
172
+
173
+ Under the hood, we use the [Tracking real method calls](#tracking-real-method-calls) feature described below.
174
+
175
+ **IMPORTANT**: Only objects declared within mock contexts could be type-checked.
176
+
177
+ ### Limitations
178
+
179
+ Currently, this feature only works if both real objects and mock objects calls are made during the same test run. Thus, tests could fail when running tests in parallel.
180
+
181
+ ## Mock contracts verification
182
+
183
+ Types drastically increase mocks/stubs stability (or consistency), but even they do not guarantee that mocks behave the same way as real objects. For example, if your method returns completely different results depending on the values (not types) of the input.
184
+
185
+ The only way to provide ~100% confidence to mocks is enforcing a contract. One way to enforce mock contracts is to require having a unit/functional tests where a real object receives the same input and returns the same result as the mock. For example, consider the following tests:
186
+
187
+ ```ruby
188
+ describe Accountant do
189
+ let(:calculator) { instance_double("TaxCalculator") }
190
+
191
+ # Declaring a mock == declaring a contract (input/output correspondance)
192
+ before do
193
+ allow(calculator).to receive(:tax_for_income).with(2020).and_return(202)
194
+ allow(calculator).to receive(:tax_for_income).with(0).and_return(0)
195
+ end
196
+
197
+ subject { described_class.new(calculator) }
198
+
199
+ specify "#after_taxes" do
200
+ # Assuming the #after_taxes method calls calculator.tax_for_income
201
+ expect(subject.after_taxes(2020)).to eq(1818)
202
+ expect(subject.after_taxes(0)).to be_nil
203
+ end
204
+ end
205
+
206
+ describe TaxCalculator do
207
+ subject { described_class.new }
208
+
209
+ # Adding a unit-test using the same input
210
+ # verifies the contract
211
+ specify "#tax_for_income" do
212
+ expect(subject.tax_for_income(2020)).to eq(202)
213
+ expect(subject.tax_for_income(0)).to eq(0)
214
+ end
215
+ end
216
+ ```
217
+
218
+ We need a way to enforce mock contract verification. In other words, if the dependency behaviour changes and the corresponding unit-test reflects this change, our mock should be marked as invalid and result into a test suit failure.
219
+
220
+ One way to do this is to introduce explicit contract verification (via custom mocking mechanisms or DSL or whatever, see [bogus][] or [compact][], for example).
221
+
222
+ Mock Suey chooses another way: automatically infer mock contracts (via mock contexts) and verify them by collecting real object calls during the test run. You can enable this feature via the following configuration options:
223
+
224
+ ```ruby
225
+ MockSuey.configure do |config|
226
+ config.verify_mock_contracts = true
227
+ # Choose the real objects tracing method
228
+ config.trace_real_calls_via = :prepend # or :trace_point
229
+ end
230
+ ```
231
+
232
+ Each method stub represents a contract. For example:
233
+
234
+ ```ruby
235
+ allow(calculator).to receive(:tax_for_income).with(2020).and_return(202)
236
+ allow(calculator).to receive(:tax_for_income).with(0).and_return(0)
237
+
238
+ #=> TaxCalculator#tax_for_income: (2020) -> Integer
239
+ #=> TaxCalculator#tax_for_income: (0) -> Integer
240
+ ```
241
+
242
+ If the method behaviours changes, running tests would result in a failure if mock doesn't reflect the change:
243
+
244
+ ```ruby
245
+ # Assuming we decided to return nil for non-positive integers
246
+ specify "#tax_for_income" do
247
+ expect(subject.tax_for_income(0)).to be_nil
248
+ end
249
+ ```
250
+
251
+ The test suite will fail with the following exception:
252
+
253
+ ```sh
254
+ $ rspec accountant_spec.rb
255
+
256
+ ........
257
+
258
+ 1) Mock contract verification failed:
259
+ No matching call found for:
260
+ TaxCalculator#tax_for_income: (0) -> Integer
261
+ Captured calls:
262
+ (0) -> NilClass
263
+ ```
264
+
265
+ The contract describes which explicit input values result in a particular output type (not value). Such verification can help to verify boundary conditions (e.g., when some inputs result in nil results or exceptions).
266
+
267
+ ### Limitations
268
+
269
+ 1. Currently, verification takes into account only primitive values (String, Number, Booleans, plain Ruby hashes and arrays, etc.). Custom classes are not yet supported.
270
+
271
+ 2. Similarly to auto-type checks, this feature does not yet support parallel tests execution.
272
+
273
+ ## Tracking stubbed method calls
274
+
275
+ The core functionality of this gem is the ability to hook into mocked method invocations to perform custom checks.
276
+
277
+ **NOTE:** Currently, only RSpec is supported.
278
+
279
+ You can add an after mock call callback as follows:
280
+
281
+ ```ruby
282
+ MockSuey.on_mocked_call do |call_obj|
283
+ # Do whatever you want with the method call object,
284
+ # containing the following fields:
285
+ # - receiver_class
286
+ # - method_name
287
+ # - arguments
288
+ # - return_value
289
+ end
290
+ ```
291
+
292
+ By default, MockSuey doesn't keep mocked calls, but if you want to analyze them
293
+ at the end of the test suite run, you can configure MockSuey to keep all the calls and
294
+ access them later:
295
+
296
+ ```ruby
297
+ MockSuey.configure do |config|
298
+ config.store_mocked_calls = true
299
+ end
300
+
301
+ # Them, you can access them in the after(:suite) hook, for example
302
+ RSpec.configure do |config|
303
+ config.after(:suite) do
304
+ p MockSuey.stored_mocked_calls
305
+ end
306
+ end
307
+ ```
308
+
309
+ ## Tracking real method calls
310
+
311
+ This gem provides a method call tracking functionality for non-double objects.
312
+ You can use it separately (not as a part of auto-type-checking or contract verification features). For example:
313
+
314
+ ```ruby
315
+ RSpec.configure do |config|
316
+ tracer = MockSuey::Tracer.new(via: :trace_point) # or via: :prepend
317
+
318
+ config.before(:suite) do
319
+ tracer.collect(SomeClass, %i[some_method another_method])
320
+ tracer.start!
321
+ end
322
+
323
+ config.after(:suite) do
324
+ calls = traces.stop
325
+ # where call is a call object
326
+ # similar to a mocked call object described above
327
+ end
328
+ end
329
+ ```
330
+
331
+ ## Configuration
332
+
333
+ Additional configuration options could be set:
334
+
335
+ ```ruby
336
+ MockSuey.configure do |config|
337
+ # A logger instance
338
+ config.logger = Logger.new($stdout)
339
+ # You can also specify log level and whether to colorize logs
340
+ # config.log_level = :info
341
+ # config.color = ? # Depends on the logging device
342
+ # Debug mode is a shortcut to setup an STDOUT logger with the debug level
343
+ config.debug = ENV["MOCK_SUEY_DEBUG"] == "true" # or 1, y, yes, or t
344
+ end
345
+ ```
346
+
347
+ ## Future development
348
+
349
+ I'm interested in the following contributions/discussions:
21
350
 
22
- _Coming soon_
351
+ - Figure out parallel builds
352
+ - Sorbet support
353
+ - Minitest support
354
+ - Advanced mock contracts (custom rules, custom classes support, etc.)
355
+ - Methods delegation (e.g., `.perform_asyn -> #perform`)
356
+ - Exceptions support in contracts verification
23
357
 
24
358
  ## Contributing
25
359
 
@@ -34,3 +368,7 @@ This gem is generated via [new-gem-generator](https://github.com/palkan/new-gem-
34
368
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
35
369
 
36
370
  [the-talk]: https://evilmartians.com/events/weaving-and-seaming-mocks
371
+ [rbs]: https://github.com/ruby/rbs
372
+ [fixturama]: https://github.com/nepalez/fixturama
373
+ [bogus]: https://github.com/psyho/bogus
374
+ [compact]: https://github.com/robwold/compact
@@ -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; end
85
+
86
+ def configure ; yield config; end
87
+
88
+ def logger ; config.logger; end
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,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MockSuey
4
+ module Ext
5
+ module InstanceClass
6
+ refine Class do
7
+ def instance_class ; self; end
8
+
9
+ def instance_class_name ; name; end
10
+ end
11
+
12
+ refine Class.singleton_class do
13
+ def instance_class
14
+ # TODO: replace with const_get
15
+ eval(instance_class_name) # rubocop:disable Security/Eval
16
+ end
17
+
18
+ def instance_class_name ; inspect.sub(%r{^#<Class:}, "").sub(/>$/, ""); end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MockSuey
4
+ module Ext
5
+ module RSpec
6
+ # Provide unified interface to access target class
7
+ # for different double/proxy types
8
+ refine ::RSpec::Mocks::TestDoubleProxy do
9
+ def target_class ; nil; end
10
+ end
11
+
12
+ refine ::RSpec::Mocks::PartialDoubleProxy do
13
+ def target_class ; object.class; end
14
+ end
15
+
16
+ refine ::RSpec::Mocks::VerifyingPartialDoubleProxy do
17
+ def target_class ; object.class; end
18
+ end
19
+
20
+ refine ::RSpec::Mocks::PartialClassDoubleProxy do
21
+ def target_class ; object.singleton_class; end
22
+ end
23
+
24
+ refine ::RSpec::Mocks::VerifyingPartialClassDoubleProxy do
25
+ def target_class ; object.singleton_class; end
26
+ end
27
+
28
+ refine ::RSpec::Mocks::VerifyingProxy do
29
+ def target_class ; @doubled_module.target; end
30
+ end
31
+
32
+ refine ::RSpec::Mocks::Proxy do
33
+ attr_reader :method_doubles
34
+ end
35
+ end
36
+ end
37
+ end