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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4631d070cd8722c45646c5a06453dd9eb80f34cd18dde0e0d6a5bfe0f085299f
|
4
|
+
data.tar.gz: 0a353b089256d5641fdfa426b4fd5922a06a6ca05e44fb196dbece77e9f58b62
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f676d7ebbdab9bc1874e2cda0633ba566868a8bcfc4c8319dda3bb662331a05539790e6324b105715893feaf9b071f37aa1cc27e12484f4eeea7dd0138b93efc
|
7
|
+
data.tar.gz: c7840271c1a2588100496034b06e4b0b5d1b5d62f6b36fc0813113aa8f0232a3c475ee5aa52cd1ebaad67123d36bb39ed64cfc13e5ce4805b0d9f8ae8ce824b1
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -2,12 +2,27 @@
|
|
2
2
|
|
3
3
|
# Mock Suey
|
4
4
|
|
5
|
-
|
5
|
+
<img align="right" height="168" width="120"
|
6
|
+
title="Mock Suey logo" src="./assets/logo.png">
|
6
7
|
|
7
|
-
|
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
|
-
|
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
|
-
|
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
|