servactory 3.1.0.rc2 → 3.1.0.rc4
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/LICENSE +21 -0
- data/README.md +7 -2
- data/config/locales/ja.yml +134 -0
- data/lib/generators/README.md +2 -2
- data/lib/generators/servactory/extension/USAGE +1 -1
- data/lib/generators/servactory/install/USAGE +2 -2
- data/lib/generators/servactory/rspec/USAGE +4 -4
- data/lib/generators/servactory/rspec/rspec_generator.rb +2 -2
- data/lib/generators/servactory/service/USAGE +5 -5
- data/lib/servactory/actions/collection.rb +21 -5
- data/lib/servactory/actions/stages/collection.rb +19 -1
- data/lib/servactory/actions/tools/rules.rb +1 -1
- data/lib/servactory/actions/tools/runner.rb +1 -1
- data/lib/servactory/configuration/collection_mode/class_names_collection.rb +1 -1
- data/lib/servactory/configuration/config.rb +16 -0
- data/lib/servactory/configuration/configurable.rb +7 -4
- data/lib/servactory/configuration/factory.rb +30 -3
- data/lib/servactory/configuration/hash_mode/class_names_collection.rb +5 -1
- data/lib/servactory/configuration/option_helpers/option_helpers_collection.rb +156 -29
- data/lib/servactory/context/warehouse/inputs.rb +1 -1
- data/lib/servactory/context/workspace/inputs.rb +2 -2
- data/lib/servactory/context/workspace/internals.rb +2 -2
- data/lib/servactory/context/workspace/outputs.rb +2 -2
- data/lib/servactory/context/workspace.rb +7 -7
- data/lib/servactory/info/builder.rb +4 -3
- data/lib/servactory/inputs/dsl.rb +5 -1
- data/lib/servactory/inputs/input.rb +1 -2
- data/lib/servactory/inputs/tools/validation.rb +10 -18
- data/lib/servactory/internals/dsl.rb +7 -1
- data/lib/servactory/maintenance/attributes/base.rb +4 -1
- data/lib/servactory/maintenance/attributes/collection.rb +61 -6
- data/lib/servactory/maintenance/options/collection.rb +71 -30
- data/lib/servactory/maintenance/options/option.rb +3 -12
- data/lib/servactory/maintenance/options/registrar.rb +3 -13
- data/lib/servactory/maintenance/validations/checkers/type.rb +23 -5
- data/lib/servactory/maintenance/validations/performer.rb +12 -23
- data/lib/servactory/outputs/dsl.rb +7 -1
- data/lib/servactory/result.rb +21 -8
- data/lib/servactory/test_kit/rspec/helpers/exception_validator.rb +112 -0
- data/lib/servactory/test_kit/rspec/helpers/input_validator.rb +3 -1
- data/lib/servactory/test_kit/rspec/helpers/mock_executor.rb +125 -125
- data/lib/servactory/test_kit/rspec/helpers/service_inputs_guard.rb +98 -0
- data/lib/servactory/test_kit/rspec/helpers/service_inputs_matcher.rb +75 -0
- data/lib/servactory/test_kit/rspec/helpers/service_mock_builder.rb +185 -50
- data/lib/servactory/test_kit/rspec/helpers/service_mock_config.rb +45 -10
- data/lib/servactory/test_kit/rspec/helpers/wrap_block_adapter.rb +74 -0
- data/lib/servactory/test_kit/rspec/helpers.rb +4 -0
- data/lib/servactory/test_kit/rspec/matchers/base/attribute_matcher.rb +72 -16
- data/lib/servactory/test_kit/rspec/matchers/base/message_expectation.rb +194 -0
- data/lib/servactory/test_kit/rspec/matchers/base/submatcher.rb +8 -0
- data/lib/servactory/test_kit/rspec/matchers/concerns/option_helper_rules.rb +148 -0
- data/lib/servactory/test_kit/rspec/matchers/concerns/proc_message.rb +117 -0
- data/lib/servactory/test_kit/rspec/matchers/concerns/required_message.rb +92 -0
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matcher.rb +27 -4
- data/lib/servactory/test_kit/rspec/matchers/have_service_internal_matcher.rb +10 -3
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/required_submatcher.rb +58 -23
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/valid_with_submatcher.rb +124 -59
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/message_submatcher.rb +133 -39
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/must_submatcher.rb +192 -17
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/target_submatcher.rb +4 -2
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/types_submatcher.rb +6 -0
- data/lib/servactory/test_kit/rspec/matchers.rb +15 -0
- data/lib/servactory/tool_kit/dynamic_options/consists_of.rb +34 -11
- data/lib/servactory/tool_kit/dynamic_options/format.rb +3 -4
- data/lib/servactory/tool_kit/dynamic_options/inclusion.rb +3 -3
- data/lib/servactory/tool_kit/dynamic_options/max.rb +3 -3
- data/lib/servactory/tool_kit/dynamic_options/min.rb +3 -3
- data/lib/servactory/tool_kit/dynamic_options/multiple_of.rb +35 -7
- data/lib/servactory/tool_kit/dynamic_options/must.rb +2 -3
- data/lib/servactory/tool_kit/dynamic_options/schema.rb +40 -25
- data/lib/servactory/tool_kit/dynamic_options/target.rb +4 -4
- data/lib/servactory/utils.rb +13 -3
- data/lib/servactory/version.rb +1 -1
- data/lib/servactory.rb +3 -4
- metadata +20 -4
|
@@ -35,6 +35,18 @@ module Servactory
|
|
|
35
35
|
# .fails(CustomException, type: :base, message: "Error")
|
|
36
36
|
# ```
|
|
37
37
|
#
|
|
38
|
+
# Pass-through to the original service (spy pattern):
|
|
39
|
+
#
|
|
40
|
+
# ```ruby
|
|
41
|
+
# allow_service(MyService)
|
|
42
|
+
# .and_call_original
|
|
43
|
+
#
|
|
44
|
+
# allow_service(MyService)
|
|
45
|
+
# .and_wrap_original do |original, **inputs|
|
|
46
|
+
# original.call(**inputs)
|
|
47
|
+
# end
|
|
48
|
+
# ```
|
|
49
|
+
#
|
|
38
50
|
# Sequential returns (first call succeeds, second fails):
|
|
39
51
|
#
|
|
40
52
|
# ```ruby
|
|
@@ -57,12 +69,30 @@ module Servactory
|
|
|
57
69
|
# .with(user_id: 123)
|
|
58
70
|
# ```
|
|
59
71
|
#
|
|
72
|
+
# Configuring the mock after the service has been called registers
|
|
73
|
+
# a new stub, which takes precedence over the earlier one for the calls
|
|
74
|
+
# it matches.
|
|
75
|
+
#
|
|
76
|
+
# A mock without `.with()` (default mock) accepts any arguments on the
|
|
77
|
+
# RSpec level, so it handles every later call of the stubbed method,
|
|
78
|
+
# including calls that mocks configured before it would match. Calls
|
|
79
|
+
# with inputs the service would not accept are rejected instead of
|
|
80
|
+
# deferred to those earlier mocks. Mocks with `.with()` configured after
|
|
81
|
+
# a default mock take precedence for the calls they match:
|
|
82
|
+
#
|
|
83
|
+
# ```ruby
|
|
84
|
+
# allow_service(MyService).succeeds(user: default_user)
|
|
85
|
+
# allow_service(MyService).with(user_id: 123).succeeds(user: user)
|
|
86
|
+
# ```
|
|
87
|
+
#
|
|
60
88
|
# ## Features
|
|
61
89
|
#
|
|
62
90
|
# - **Fluent API** - chainable methods for readable test setup
|
|
63
91
|
# - **Success/Failure** - configure expected result type in one method
|
|
64
92
|
# - **Exception Handling** - auto-creates exceptions with type, message, meta
|
|
65
93
|
# - **Input Matching** - match specific service inputs with `.with()`
|
|
94
|
+
# - **Pass-Through** - delegate to the original service with `.and_call_original`
|
|
95
|
+
# or `.and_wrap_original`
|
|
66
96
|
# - **Sequential Responses** - different results for consecutive calls
|
|
67
97
|
# - **Automatic Validation** - validates inputs and outputs against service definition
|
|
68
98
|
#
|
|
@@ -77,6 +107,15 @@ module Servactory
|
|
|
77
107
|
include Concerns::ServiceClassValidation
|
|
78
108
|
include Concerns::ErrorMessages
|
|
79
109
|
|
|
110
|
+
RESULT_TYPE_TO_METHOD = {
|
|
111
|
+
success: :succeeds,
|
|
112
|
+
failure: :fails,
|
|
113
|
+
call_original: :and_call_original,
|
|
114
|
+
wrap_original: :and_wrap_original
|
|
115
|
+
}.freeze
|
|
116
|
+
|
|
117
|
+
private_constant :RESULT_TYPE_TO_METHOD
|
|
118
|
+
|
|
80
119
|
# @return [Class] The Servactory service class being mocked
|
|
81
120
|
attr_reader :service_class
|
|
82
121
|
|
|
@@ -96,7 +135,7 @@ module Servactory
|
|
|
96
135
|
@config = ServiceMockConfig.new(service_class:)
|
|
97
136
|
@config.method_type = method_type
|
|
98
137
|
@sequential_configs = []
|
|
99
|
-
@
|
|
138
|
+
@stub = nil
|
|
100
139
|
end
|
|
101
140
|
|
|
102
141
|
# ============================================================
|
|
@@ -127,7 +166,7 @@ module Servactory
|
|
|
127
166
|
validate_outputs!(outputs_hash)
|
|
128
167
|
@config.result_type = :success
|
|
129
168
|
@config.outputs = outputs_hash
|
|
130
|
-
|
|
169
|
+
execute_mock
|
|
131
170
|
self
|
|
132
171
|
end
|
|
133
172
|
|
|
@@ -157,7 +196,7 @@ module Servactory
|
|
|
157
196
|
|
|
158
197
|
@config.result_type = :failure
|
|
159
198
|
@config.exception = build_exception(exception_class, type:, message:, meta:)
|
|
160
|
-
|
|
199
|
+
execute_mock
|
|
161
200
|
self
|
|
162
201
|
end
|
|
163
202
|
|
|
@@ -168,6 +207,11 @@ module Servactory
|
|
|
168
207
|
#
|
|
169
208
|
# Inputs are automatically validated against service definition.
|
|
170
209
|
#
|
|
210
|
+
# Without `.with()`, a call matches when all required inputs are present
|
|
211
|
+
# and no unknown inputs are passed; optional inputs may be omitted.
|
|
212
|
+
# Such a mock handles every later call of the stubbed method and rejects
|
|
213
|
+
# invalid inputs instead of deferring to mocks configured before it.
|
|
214
|
+
#
|
|
171
215
|
# @param inputs_hash_or_matcher [Hash, Object] Service inputs to match or RSpec matcher
|
|
172
216
|
# @return [ServiceMockBuilder] self for method chaining
|
|
173
217
|
#
|
|
@@ -183,8 +227,73 @@ module Servactory
|
|
|
183
227
|
# @raise [InputValidator::ValidationError] if inputs don't match service definition
|
|
184
228
|
def with(inputs_hash_or_matcher)
|
|
185
229
|
validate_inputs!(inputs_hash_or_matcher)
|
|
186
|
-
|
|
187
|
-
|
|
230
|
+
all_configs.each { |config| config.argument_matcher = inputs_hash_or_matcher }
|
|
231
|
+
update_mock_arguments
|
|
232
|
+
self
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# ============================================================
|
|
236
|
+
# RSpec Pass-Through API
|
|
237
|
+
# ============================================================
|
|
238
|
+
|
|
239
|
+
# Delegates to the original unmodified method.
|
|
240
|
+
# Useful for spy pattern or selective mocking.
|
|
241
|
+
#
|
|
242
|
+
# @return [ServiceMockBuilder] self for method chaining
|
|
243
|
+
#
|
|
244
|
+
# @example Pass-through (spy)
|
|
245
|
+
# allow_service(S).and_call_original
|
|
246
|
+
#
|
|
247
|
+
# @example Selective mocking with input matching
|
|
248
|
+
# allow_service(S).with(id: 1).and_call_original
|
|
249
|
+
def and_call_original
|
|
250
|
+
validate_not_in_sequential_mode!(:and_call_original)
|
|
251
|
+
validate_result_type_not_switched!(:and_call_original)
|
|
252
|
+
|
|
253
|
+
@config.result_type = :call_original
|
|
254
|
+
execute_mock
|
|
255
|
+
self
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Wraps the original method with custom logic.
|
|
259
|
+
# Block receives the original method and the service inputs as keywords,
|
|
260
|
+
# whether the service was called with keywords, a positional Hash or
|
|
261
|
+
# a Datory object. Input names are symbolized the way the service does it.
|
|
262
|
+
# A block without keyword parameters receives the call arguments as given,
|
|
263
|
+
# e.g. `{}` for `.call({})`.
|
|
264
|
+
#
|
|
265
|
+
# @yield [original, **inputs] Block wrapping the original
|
|
266
|
+
# @yieldparam original [Method] The original `.call` or `.call!` method
|
|
267
|
+
# @yieldparam inputs [Hash{Symbol => Object}] Service inputs
|
|
268
|
+
# @yieldreturn [Object] Value returned to the service caller
|
|
269
|
+
# @return [ServiceMockBuilder] self for method chaining
|
|
270
|
+
#
|
|
271
|
+
# @example Modify result
|
|
272
|
+
# allow_service(S).and_wrap_original do |original, **inputs|
|
|
273
|
+
# result = original.call(**inputs)
|
|
274
|
+
# # custom logic
|
|
275
|
+
# result
|
|
276
|
+
# end
|
|
277
|
+
#
|
|
278
|
+
# @example Modify inputs
|
|
279
|
+
# allow_service(S).and_wrap_original do |original, **inputs|
|
|
280
|
+
# original.call(**inputs, locale: "en")
|
|
281
|
+
# end
|
|
282
|
+
#
|
|
283
|
+
# @example Receive the call arguments as given
|
|
284
|
+
# allow_service(S).and_wrap_original do |original, *arguments|
|
|
285
|
+
# original.call(*arguments)
|
|
286
|
+
# end
|
|
287
|
+
#
|
|
288
|
+
# @raise [ArgumentError] if called without a block
|
|
289
|
+
def and_wrap_original(&block)
|
|
290
|
+
validate_block_given!(:and_wrap_original, block)
|
|
291
|
+
validate_not_in_sequential_mode!(:and_wrap_original)
|
|
292
|
+
validate_result_type_not_switched!(:and_wrap_original)
|
|
293
|
+
|
|
294
|
+
@config.result_type = :wrap_original
|
|
295
|
+
@config.wrap_block = block
|
|
296
|
+
execute_mock
|
|
188
297
|
self
|
|
189
298
|
end
|
|
190
299
|
|
|
@@ -208,6 +317,7 @@ module Servactory
|
|
|
208
317
|
# @raise [ArgumentError] if called without first calling succeeds/fails
|
|
209
318
|
# @raise [OutputValidator::ValidationError] if outputs don't match service definition
|
|
210
319
|
def then_succeeds(outputs_hash = {})
|
|
320
|
+
validate_not_passthrough!(:then_succeeds)
|
|
211
321
|
validate_result_type_defined!(:then_succeeds)
|
|
212
322
|
|
|
213
323
|
validate_outputs!(outputs_hash)
|
|
@@ -216,7 +326,7 @@ module Servactory
|
|
|
216
326
|
@config.result_type = :success
|
|
217
327
|
@config.outputs = outputs_hash
|
|
218
328
|
@config.method_type = @sequential_configs.last&.method_type || :call
|
|
219
|
-
|
|
329
|
+
execute_mock
|
|
220
330
|
self
|
|
221
331
|
end
|
|
222
332
|
|
|
@@ -237,6 +347,7 @@ module Servactory
|
|
|
237
347
|
#
|
|
238
348
|
# @raise [ArgumentError] if called without first calling succeeds/fails
|
|
239
349
|
def then_fails(exception_class = nil, type: :base, message:, meta: nil) # rubocop:disable Style/KeywordParametersOrder
|
|
350
|
+
validate_not_passthrough!(:then_fails)
|
|
240
351
|
validate_result_type_defined!(:then_fails)
|
|
241
352
|
|
|
242
353
|
finalize_current_to_sequence
|
|
@@ -244,7 +355,7 @@ module Servactory
|
|
|
244
355
|
@config.result_type = :failure
|
|
245
356
|
@config.exception = build_exception(exception_class, type:, message:, meta:)
|
|
246
357
|
@config.method_type = @sequential_configs.last&.method_type || :call
|
|
247
|
-
|
|
358
|
+
execute_mock
|
|
248
359
|
self
|
|
249
360
|
end
|
|
250
361
|
|
|
@@ -281,22 +392,51 @@ module Servactory
|
|
|
281
392
|
|
|
282
393
|
# Validates that result type is not being switched.
|
|
283
394
|
#
|
|
284
|
-
# Prevents accidental switching between
|
|
395
|
+
# Prevents accidental switching between different result types.
|
|
285
396
|
#
|
|
286
|
-
# @param new_type [Symbol] :succeeds or :
|
|
397
|
+
# @param new_type [Symbol] :succeeds, :fails, :and_call_original, or :and_wrap_original
|
|
287
398
|
# @raise [ArgumentError] if trying to switch result type
|
|
288
399
|
# @return [void]
|
|
289
|
-
def validate_result_type_not_switched!(new_type)
|
|
400
|
+
def validate_result_type_not_switched!(new_type)
|
|
290
401
|
return unless @config.result_type_defined?
|
|
291
402
|
|
|
292
|
-
current_type = @config.
|
|
293
|
-
return if
|
|
294
|
-
(new_type == :fails && current_type == :fails)
|
|
403
|
+
current_type = RESULT_TYPE_TO_METHOD[@config.result_type]
|
|
404
|
+
return if new_type == current_type
|
|
295
405
|
|
|
296
406
|
raise ArgumentError,
|
|
297
407
|
"Cannot call #{new_type}() after #{current_type}() was already called. " \
|
|
298
|
-
"
|
|
299
|
-
"
|
|
408
|
+
"This replaces the result type, which is likely a mistake. " \
|
|
409
|
+
"Create a new mock if you need different behavior."
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Validates that pass-through methods are not mixed with sequential responses.
|
|
413
|
+
#
|
|
414
|
+
# @param method_name [Symbol] The method being called
|
|
415
|
+
# @raise [ArgumentError] if current config is a pass-through type
|
|
416
|
+
# @return [void]
|
|
417
|
+
def validate_not_passthrough!(method_name)
|
|
418
|
+
return unless @config.call_original? || @config.wrap_original?
|
|
419
|
+
|
|
420
|
+
passthrough = @config.call_original? ? :and_call_original : :and_wrap_original
|
|
421
|
+
|
|
422
|
+
raise ArgumentError,
|
|
423
|
+
"Cannot call #{method_name}() after #{passthrough}(). " \
|
|
424
|
+
"Pass-through methods are not compatible with sequential responses."
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Validates that a block is given.
|
|
428
|
+
#
|
|
429
|
+
# @param method_name [Symbol] The method being called
|
|
430
|
+
# @param block [Proc, nil] The block given to the method
|
|
431
|
+
# @raise [ArgumentError] if block is missing
|
|
432
|
+
# @return [void]
|
|
433
|
+
def validate_block_given!(method_name, block)
|
|
434
|
+
return unless block.nil?
|
|
435
|
+
|
|
436
|
+
raise ArgumentError,
|
|
437
|
+
"Cannot call #{method_name}() without a block. " \
|
|
438
|
+
"Pass a block that receives the original method and the service inputs, " \
|
|
439
|
+
"e.g. { |original, **inputs| original.call(**inputs) }."
|
|
300
440
|
end
|
|
301
441
|
|
|
302
442
|
# Validates outputs against service definition.
|
|
@@ -354,17 +494,6 @@ module Servactory
|
|
|
354
494
|
# Mock Execution
|
|
355
495
|
# ============================================================
|
|
356
496
|
|
|
357
|
-
# Executes or re-executes mock depending on current state.
|
|
358
|
-
#
|
|
359
|
-
# @return [void]
|
|
360
|
-
def execute_or_re_execute_mock
|
|
361
|
-
if @executed
|
|
362
|
-
re_execute_mock
|
|
363
|
-
else
|
|
364
|
-
execute_mock
|
|
365
|
-
end
|
|
366
|
-
end
|
|
367
|
-
|
|
368
497
|
# Saves current config to sequential list.
|
|
369
498
|
#
|
|
370
499
|
# @return [void]
|
|
@@ -372,48 +501,54 @@ module Servactory
|
|
|
372
501
|
@sequential_configs << @config.dup
|
|
373
502
|
end
|
|
374
503
|
|
|
375
|
-
#
|
|
504
|
+
# Returns all configs in call order.
|
|
505
|
+
#
|
|
506
|
+
# @return [Array<ServiceMockConfig>] Sequential configs followed by the current config
|
|
507
|
+
def all_configs
|
|
508
|
+
@sequential_configs + [@config]
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# Registers the mock, or reconfigures the already registered stub.
|
|
512
|
+
#
|
|
513
|
+
# A stub that has already been invoked cannot be reconfigured,
|
|
514
|
+
# so a new stub is registered instead.
|
|
376
515
|
#
|
|
377
516
|
# @return [void]
|
|
378
517
|
def execute_mock
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
@executed = true
|
|
382
|
-
MockExecutor.new(
|
|
383
|
-
service_class:,
|
|
384
|
-
configs: [@config],
|
|
385
|
-
rspec_context: @rspec_context
|
|
386
|
-
).execute
|
|
518
|
+
@stub = mock_executor.execute(reconfigurable_stub)
|
|
387
519
|
end
|
|
388
520
|
|
|
389
|
-
#
|
|
521
|
+
# Applies the current argument matcher to the registered stub.
|
|
522
|
+
#
|
|
523
|
+
# Registers a new stub if the registered one has already been invoked.
|
|
390
524
|
#
|
|
391
525
|
# @return [void]
|
|
392
|
-
def
|
|
393
|
-
return
|
|
526
|
+
def update_mock_arguments
|
|
527
|
+
return if @stub.nil?
|
|
394
528
|
|
|
395
|
-
if @
|
|
396
|
-
|
|
529
|
+
if @stub.invoked?
|
|
530
|
+
execute_mock
|
|
397
531
|
else
|
|
398
|
-
|
|
399
|
-
service_class:,
|
|
400
|
-
configs: [@config],
|
|
401
|
-
rspec_context: @rspec_context
|
|
402
|
-
).execute
|
|
532
|
+
mock_executor.update_arguments(@stub)
|
|
403
533
|
end
|
|
404
534
|
end
|
|
405
535
|
|
|
406
|
-
#
|
|
536
|
+
# Returns the registered stub unless it has already been invoked.
|
|
407
537
|
#
|
|
408
|
-
# @return [
|
|
409
|
-
def
|
|
410
|
-
|
|
538
|
+
# @return [MockExecutor::Stub, nil] Stub that can be reconfigured in place
|
|
539
|
+
def reconfigurable_stub
|
|
540
|
+
@stub unless @stub&.invoked?
|
|
541
|
+
end
|
|
411
542
|
|
|
543
|
+
# Builds an executor for the current configs.
|
|
544
|
+
#
|
|
545
|
+
# @return [MockExecutor] Executor for all configs
|
|
546
|
+
def mock_executor
|
|
412
547
|
MockExecutor.new(
|
|
413
548
|
service_class:,
|
|
414
549
|
configs: all_configs,
|
|
415
550
|
rspec_context: @rspec_context
|
|
416
|
-
)
|
|
551
|
+
)
|
|
417
552
|
end
|
|
418
553
|
end
|
|
419
554
|
end
|
|
@@ -9,9 +9,9 @@ module Servactory
|
|
|
9
9
|
# ## Purpose
|
|
10
10
|
#
|
|
11
11
|
# Holds all configuration for mocking a single service call, including
|
|
12
|
-
# result type (success/failure), method type (call/call!),
|
|
13
|
-
# exceptions,
|
|
14
|
-
# MockExecutor.
|
|
12
|
+
# result type (success/failure/pass-through), method type (call/call!),
|
|
13
|
+
# outputs, exceptions, argument matchers, and the wrap block.
|
|
14
|
+
# Used by ServiceMockBuilder and MockExecutor.
|
|
15
15
|
#
|
|
16
16
|
# ## Usage
|
|
17
17
|
#
|
|
@@ -27,18 +27,20 @@ module Servactory
|
|
|
27
27
|
# ## Attributes
|
|
28
28
|
#
|
|
29
29
|
# - `service_class` - The service class being mocked
|
|
30
|
-
# - `result_type` - :success or :
|
|
30
|
+
# - `result_type` - :success, :failure, :call_original, or :wrap_original
|
|
31
31
|
# - `method_type` - :call or :call!
|
|
32
32
|
# - `outputs` - Hash of output values
|
|
33
33
|
# - `exception` - Exception for failure mocks
|
|
34
34
|
# - `argument_matcher` - RSpec argument matcher or Hash
|
|
35
|
+
# - `wrap_block` - Block for :wrap_original mocks
|
|
35
36
|
class ServiceMockConfig
|
|
36
37
|
attr_accessor :service_class,
|
|
37
38
|
:result_type,
|
|
38
39
|
:method_type,
|
|
39
40
|
:outputs,
|
|
40
41
|
:exception,
|
|
41
|
-
:argument_matcher
|
|
42
|
+
:argument_matcher,
|
|
43
|
+
:wrap_block
|
|
42
44
|
|
|
43
45
|
# Creates a new mock configuration.
|
|
44
46
|
#
|
|
@@ -51,6 +53,7 @@ module Servactory
|
|
|
51
53
|
@outputs = {}
|
|
52
54
|
@exception = nil
|
|
53
55
|
@argument_matcher = nil
|
|
56
|
+
@wrap_block = nil
|
|
54
57
|
end
|
|
55
58
|
|
|
56
59
|
# Checks if this is a success mock.
|
|
@@ -67,6 +70,27 @@ module Servactory
|
|
|
67
70
|
result_type == :failure
|
|
68
71
|
end
|
|
69
72
|
|
|
73
|
+
# Checks if this is a call_original mock.
|
|
74
|
+
#
|
|
75
|
+
# @return [Boolean] True if result_type is :call_original
|
|
76
|
+
def call_original?
|
|
77
|
+
result_type == :call_original
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Checks if this is a wrap_original mock.
|
|
81
|
+
#
|
|
82
|
+
# @return [Boolean] True if result_type is :wrap_original
|
|
83
|
+
def wrap_original?
|
|
84
|
+
result_type == :wrap_original
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Checks if this is a pass-through mock.
|
|
88
|
+
#
|
|
89
|
+
# @return [Boolean] True if result_type is :call_original or :wrap_original
|
|
90
|
+
def pass_through?
|
|
91
|
+
call_original? || wrap_original?
|
|
92
|
+
end
|
|
93
|
+
|
|
70
94
|
# Checks if this mocks the .call! method.
|
|
71
95
|
#
|
|
72
96
|
# @return [Boolean] True if method_type is :call!
|
|
@@ -97,29 +121,40 @@ module Servactory
|
|
|
97
121
|
|
|
98
122
|
# Builds RSpec argument matcher from config.
|
|
99
123
|
#
|
|
100
|
-
# If no matcher specified,
|
|
124
|
+
# If no matcher specified, any arguments are accepted by RSpec
|
|
125
|
+
# and verified against the matcher from #build_inputs_matcher.
|
|
101
126
|
#
|
|
102
127
|
# @param rspec_context [Object] The RSpec test context
|
|
103
128
|
# @return [Object] RSpec argument matcher
|
|
104
129
|
def build_argument_matcher(rspec_context)
|
|
105
130
|
return argument_matcher if argument_matcher.present?
|
|
106
131
|
|
|
107
|
-
|
|
108
|
-
|
|
132
|
+
rspec_context.any_args
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Builds matcher of service inputs used when no argument matcher is specified.
|
|
136
|
+
#
|
|
137
|
+
# Required inputs must be present, optional inputs may be omitted,
|
|
138
|
+
# and unknown inputs are rejected.
|
|
139
|
+
#
|
|
140
|
+
# @return [ServiceInputsMatcher, nil] Inputs matcher, or nil if an argument matcher is specified
|
|
141
|
+
def build_inputs_matcher
|
|
142
|
+
return if argument_matcher.present?
|
|
109
143
|
|
|
110
|
-
|
|
144
|
+
ServiceInputsMatcher.new(service_class.info.inputs)
|
|
111
145
|
end
|
|
112
146
|
|
|
113
147
|
# Creates a deep copy of this config.
|
|
114
148
|
#
|
|
115
149
|
# @return [ServiceMockConfig] New config with copied values
|
|
116
|
-
def dup
|
|
150
|
+
def dup # rubocop:disable Metrics/AbcSize
|
|
117
151
|
copy = self.class.new(service_class:)
|
|
118
152
|
copy.result_type = result_type
|
|
119
153
|
copy.method_type = method_type
|
|
120
154
|
copy.outputs = outputs.dup
|
|
121
155
|
copy.exception = exception
|
|
122
156
|
copy.argument_matcher = argument_matcher
|
|
157
|
+
copy.wrap_block = wrap_block
|
|
123
158
|
copy
|
|
124
159
|
end
|
|
125
160
|
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Helpers
|
|
7
|
+
# Adapts blocks given to `and_wrap_original` to the arguments of service calls.
|
|
8
|
+
#
|
|
9
|
+
# ## Purpose
|
|
10
|
+
#
|
|
11
|
+
# Services accept inputs as keywords, as a positional Hash or as
|
|
12
|
+
# a Datory object. A block accepting keywords receives the inputs as
|
|
13
|
+
# keywords in each case, converted with Servactory::Utils.adapt the way
|
|
14
|
+
# the service converts them, so String keys and HashWithIndifferentAccess
|
|
15
|
+
# arrive as Symbol keys. Any other block receives the arguments as given,
|
|
16
|
+
# the way RSpec's `and_wrap_original` passes them, so a call with an
|
|
17
|
+
# empty positional Hash passes `{}` to it.
|
|
18
|
+
#
|
|
19
|
+
# The adaptation is chosen once from the block parameters.
|
|
20
|
+
#
|
|
21
|
+
# ## Usage
|
|
22
|
+
#
|
|
23
|
+
# Used internally by MockExecutor:
|
|
24
|
+
#
|
|
25
|
+
# ```ruby
|
|
26
|
+
# delegate = WrapBlockAdapter.adapt(wrap_block)
|
|
27
|
+
#
|
|
28
|
+
# delegate.call(original, *arguments, &block)
|
|
29
|
+
# ```
|
|
30
|
+
class WrapBlockAdapter
|
|
31
|
+
KEYWORD_PARAMETER_TYPES = %i[key keyreq keyrest].freeze
|
|
32
|
+
|
|
33
|
+
private_constant :KEYWORD_PARAMETER_TYPES
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
# Adapts a wrap block to the arguments of service calls.
|
|
37
|
+
#
|
|
38
|
+
# @param wrap_block [Proc] Block given to and_wrap_original
|
|
39
|
+
# @return [Proc] Callable receiving the original method and the call arguments
|
|
40
|
+
def adapt(wrap_block)
|
|
41
|
+
return wrap_block unless accepts_keywords?(wrap_block)
|
|
42
|
+
|
|
43
|
+
with_keyword_inputs(wrap_block)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# Checks whether a block declares keyword parameters.
|
|
49
|
+
#
|
|
50
|
+
# @param wrap_block [Proc] Block given to and_wrap_original
|
|
51
|
+
# @return [Boolean] True if the block accepts keywords
|
|
52
|
+
def accepts_keywords?(wrap_block)
|
|
53
|
+
wrap_block.parameters.any? { |type, _name| KEYWORD_PARAMETER_TYPES.include?(type) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Adapts a block to receive service inputs as keywords.
|
|
57
|
+
#
|
|
58
|
+
# @param wrap_block [Proc] Block accepting keywords
|
|
59
|
+
# @return [Proc] Callable passing the inputs of a call to the block as Symbol keywords
|
|
60
|
+
def with_keyword_inputs(wrap_block)
|
|
61
|
+
lambda do |original, *arguments, &block|
|
|
62
|
+
if arguments.one? && Servactory::Utils.adaptable?(arguments.first)
|
|
63
|
+
wrap_block.call(original, **Servactory::Utils.adapt(arguments.first), &block)
|
|
64
|
+
else
|
|
65
|
+
wrap_block.call(original, *arguments, &block)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "helpers/concerns/error_messages"
|
|
4
4
|
require_relative "helpers/concerns/service_class_validation"
|
|
5
|
+
require_relative "helpers/service_inputs_matcher"
|
|
6
|
+
require_relative "helpers/service_inputs_guard"
|
|
5
7
|
require_relative "helpers/service_mock_config"
|
|
8
|
+
require_relative "helpers/exception_validator"
|
|
9
|
+
require_relative "helpers/wrap_block_adapter"
|
|
6
10
|
require_relative "helpers/mock_executor"
|
|
7
11
|
require_relative "helpers/input_validator"
|
|
8
12
|
require_relative "helpers/output_validator"
|