cmdx 1.1.0 → 1.1.1
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/.cursor/prompts/docs.md +9 -0
- data/.cursor/prompts/rspec.md +13 -12
- data/.cursor/prompts/yardoc.md +11 -6
- data/CHANGELOG.md +13 -2
- data/README.md +1 -0
- data/docs/ai_prompts.md +269 -195
- data/docs/basics/call.md +124 -58
- data/docs/basics/chain.md +190 -160
- data/docs/basics/context.md +242 -154
- data/docs/basics/setup.md +302 -32
- data/docs/callbacks.md +390 -94
- data/docs/configuration.md +181 -65
- data/docs/deprecation.md +245 -0
- data/docs/getting_started.md +161 -39
- data/docs/internationalization.md +590 -70
- data/docs/interruptions/exceptions.md +135 -118
- data/docs/interruptions/faults.md +150 -125
- data/docs/interruptions/halt.md +134 -80
- data/docs/logging.md +181 -118
- data/docs/middlewares.md +150 -377
- data/docs/outcomes/result.md +140 -112
- data/docs/outcomes/states.md +134 -99
- data/docs/outcomes/statuses.md +204 -146
- data/docs/parameters/coercions.md +232 -281
- data/docs/parameters/defaults.md +224 -169
- data/docs/parameters/definitions.md +289 -141
- data/docs/parameters/namespacing.md +250 -161
- data/docs/parameters/validations.md +260 -133
- data/docs/testing.md +191 -197
- data/docs/workflows.md +143 -98
- data/lib/cmdx/callback.rb +23 -19
- data/lib/cmdx/callback_registry.rb +1 -3
- data/lib/cmdx/chain_inspector.rb +23 -23
- data/lib/cmdx/chain_serializer.rb +38 -19
- data/lib/cmdx/coercion.rb +20 -12
- data/lib/cmdx/coercion_registry.rb +51 -32
- data/lib/cmdx/configuration.rb +84 -31
- data/lib/cmdx/context.rb +32 -21
- data/lib/cmdx/core_ext/hash.rb +13 -13
- data/lib/cmdx/core_ext/module.rb +1 -1
- data/lib/cmdx/core_ext/object.rb +12 -12
- data/lib/cmdx/correlator.rb +60 -39
- data/lib/cmdx/errors.rb +105 -131
- data/lib/cmdx/fault.rb +66 -45
- data/lib/cmdx/immutator.rb +20 -21
- data/lib/cmdx/lazy_struct.rb +78 -70
- data/lib/cmdx/log_formatters/json.rb +1 -1
- data/lib/cmdx/log_formatters/key_value.rb +1 -1
- data/lib/cmdx/log_formatters/line.rb +1 -1
- data/lib/cmdx/log_formatters/logstash.rb +1 -1
- data/lib/cmdx/log_formatters/pretty_json.rb +1 -1
- data/lib/cmdx/log_formatters/pretty_key_value.rb +1 -1
- data/lib/cmdx/log_formatters/pretty_line.rb +1 -1
- data/lib/cmdx/log_formatters/raw.rb +2 -2
- data/lib/cmdx/logger.rb +19 -14
- data/lib/cmdx/logger_ansi.rb +33 -17
- data/lib/cmdx/logger_serializer.rb +85 -24
- data/lib/cmdx/middleware.rb +39 -21
- data/lib/cmdx/middleware_registry.rb +4 -3
- data/lib/cmdx/parameter.rb +151 -89
- data/lib/cmdx/parameter_inspector.rb +34 -21
- data/lib/cmdx/parameter_registry.rb +36 -30
- data/lib/cmdx/parameter_serializer.rb +21 -14
- data/lib/cmdx/result.rb +136 -135
- data/lib/cmdx/result_ansi.rb +31 -17
- data/lib/cmdx/result_inspector.rb +32 -27
- data/lib/cmdx/result_logger.rb +23 -14
- data/lib/cmdx/result_serializer.rb +65 -27
- data/lib/cmdx/task.rb +234 -113
- data/lib/cmdx/task_deprecator.rb +22 -25
- data/lib/cmdx/task_processor.rb +89 -88
- data/lib/cmdx/task_serializer.rb +27 -14
- data/lib/cmdx/utils/monotonic_runtime.rb +2 -4
- data/lib/cmdx/validator.rb +25 -16
- data/lib/cmdx/validator_registry.rb +53 -31
- data/lib/cmdx/validators/exclusion.rb +1 -1
- data/lib/cmdx/validators/format.rb +2 -2
- data/lib/cmdx/validators/inclusion.rb +2 -2
- data/lib/cmdx/validators/length.rb +2 -2
- data/lib/cmdx/validators/numeric.rb +3 -3
- data/lib/cmdx/validators/presence.rb +2 -2
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx/workflow.rb +54 -33
- data/lib/generators/cmdx/task_generator.rb +6 -6
- data/lib/generators/cmdx/workflow_generator.rb +6 -6
- metadata +3 -1
data/docs/testing.md
CHANGED
@@ -10,29 +10,38 @@ CMDx provides a comprehensive suite of custom RSpec matchers designed for expres
|
|
10
10
|
- [Result Matchers](#result-matchers)
|
11
11
|
- [Primary Outcome Matchers](#primary-outcome-matchers)
|
12
12
|
- [State and Status Matchers](#state-and-status-matchers)
|
13
|
+
- [Execution and Outcome Matchers](#execution-and-outcome-matchers)
|
13
14
|
- [Metadata and Context Matchers](#metadata-and-context-matchers)
|
14
15
|
- [Failure Chain Matchers](#failure-chain-matchers)
|
15
16
|
- [Task Matchers](#task-matchers)
|
16
|
-
- [
|
17
|
-
- [
|
18
|
-
- [Exception Handling Matchers](#exception-handling-matchers)
|
17
|
+
- [Structure and Lifecycle Matchers](#structure-and-lifecycle-matchers)
|
18
|
+
- [Parameter Testing Matchers](#parameter-testing-matchers)
|
19
19
|
- [Callback and Middleware Matchers](#callback-and-middleware-matchers)
|
20
20
|
- [Configuration Matchers](#configuration-matchers)
|
21
21
|
- [Composable Testing](#composable-testing)
|
22
|
+
- [Error Handling](#error-handling)
|
22
23
|
- [Best Practices](#best-practices)
|
23
24
|
|
24
25
|
## TLDR
|
25
26
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
27
|
+
```ruby
|
28
|
+
# Setup - require in spec helper
|
29
|
+
require "cmdx/rspec/matchers"
|
30
|
+
|
31
|
+
# Result outcome matchers
|
32
|
+
expect(result).to be_successful_task(user_id: 123)
|
33
|
+
expect(result).to be_failed_task("validation_error").with_metadata(field: "email")
|
34
|
+
expect(result).to be_skipped_task.with_reason("already_processed")
|
35
|
+
|
36
|
+
# Task structure matchers
|
37
|
+
expect(MyTask).to be_well_formed_task
|
38
|
+
expect(MyTask).to have_parameter(:email).that_is_required.with_type(:string)
|
39
|
+
expect(MyTask).to have_callback(:before_execution)
|
40
|
+
```
|
32
41
|
|
33
42
|
## External Project Setup
|
34
43
|
|
35
|
-
To use CMDx's custom matchers in an external RSpec-based project update your `spec/spec_helper.rb` or `spec/rails_helper.rb`:
|
44
|
+
To use CMDx's custom matchers in an external RSpec-based project, update your `spec/spec_helper.rb` or `spec/rails_helper.rb`:
|
36
45
|
|
37
46
|
```ruby
|
38
47
|
require "cmdx/rspec/matchers"
|
@@ -40,19 +49,15 @@ require "cmdx/rspec/matchers"
|
|
40
49
|
|
41
50
|
## Matcher Organization
|
42
51
|
|
43
|
-
CMDx matchers are organized into two primary
|
52
|
+
CMDx matchers are organized into two primary categories with comprehensive YARD documentation:
|
44
53
|
|
45
|
-
| Purpose | Matcher Count |
|
46
|
-
|
47
|
-
| Task execution outcomes and side effects |
|
48
|
-
| Task behavior, validation, and lifecycle |
|
54
|
+
| Category | Purpose | Matcher Count |
|
55
|
+
|----------|---------|---------------|
|
56
|
+
| **Result Matchers** | Task execution outcomes and side effects | 17 matchers |
|
57
|
+
| **Task Matchers** | Task behavior, validation, and lifecycle | 6 matchers |
|
49
58
|
|
50
|
-
|
51
|
-
|
52
|
-
- Multiple usage examples
|
53
|
-
- Return value specifications
|
54
|
-
- Negation examples
|
55
|
-
- Version information
|
59
|
+
> [!NOTE]
|
60
|
+
> All matchers include complete parameter descriptions, multiple usage examples, return value specifications, negation examples, and version information.
|
56
61
|
|
57
62
|
## Result Matchers
|
58
63
|
|
@@ -69,15 +74,19 @@ expect(result).to be_successful_task
|
|
69
74
|
# Successful task with context validation
|
70
75
|
expect(result).to be_successful_task(user_id: 123, processed: true)
|
71
76
|
|
72
|
-
#
|
73
|
-
expect(result).
|
77
|
+
# With RSpec matchers for flexible context validation
|
78
|
+
expect(result).to be_successful_task(
|
79
|
+
user_id: be_a(Integer),
|
80
|
+
processed_at: be_a(Time),
|
81
|
+
email: match(/@/)
|
82
|
+
)
|
74
83
|
```
|
75
84
|
|
76
85
|
**What it validates:**
|
77
86
|
- Result has success status
|
78
87
|
- Result is in complete state
|
79
88
|
- Result was executed
|
80
|
-
- Optional context attributes match
|
89
|
+
- Optional context attributes match expected values
|
81
90
|
|
82
91
|
#### Failed Task Validation
|
83
92
|
|
@@ -86,15 +95,14 @@ expect(result).not_to be_successful_task
|
|
86
95
|
expect(result).to be_failed_task
|
87
96
|
|
88
97
|
# Failed task with specific reason
|
89
|
-
expect(result).to be_failed_task("
|
98
|
+
expect(result).to be_failed_task("validation_failed")
|
90
99
|
|
91
|
-
#
|
92
|
-
expect(result).to be_failed_task
|
93
|
-
.with_reason("Invalid data")
|
94
|
-
.with_metadata(error_code: "ERR001", retryable: false)
|
100
|
+
# Using with_reason chain
|
101
|
+
expect(result).to be_failed_task.with_reason("invalid_data")
|
95
102
|
|
96
|
-
#
|
97
|
-
expect(result).
|
103
|
+
# Combined reason and metadata validation
|
104
|
+
expect(result).to be_failed_task("validation_error")
|
105
|
+
.with_metadata(field: "email", rule: "format", retryable: false)
|
98
106
|
```
|
99
107
|
|
100
108
|
**What it validates:**
|
@@ -110,15 +118,14 @@ expect(result).not_to be_failed_task
|
|
110
118
|
expect(result).to be_skipped_task
|
111
119
|
|
112
120
|
# Skipped task with specific reason
|
113
|
-
expect(result).to be_skipped_task("
|
121
|
+
expect(result).to be_skipped_task("already_processed")
|
114
122
|
|
115
|
-
#
|
116
|
-
expect(result).to be_skipped_task
|
117
|
-
.with_reason("Order already processed")
|
118
|
-
.with_metadata(processed_at: be_a(Time), skip_code: "DUPLICATE")
|
123
|
+
# Using with_reason chain
|
124
|
+
expect(result).to be_skipped_task.with_reason("order_already_processed")
|
119
125
|
|
120
|
-
#
|
121
|
-
expect(result).
|
126
|
+
# Combined reason and metadata validation
|
127
|
+
expect(result).to be_skipped_task("data_unchanged")
|
128
|
+
.with_metadata(last_sync: be_a(Time), changes: 0)
|
122
129
|
```
|
123
130
|
|
124
131
|
**What it validates:**
|
@@ -134,29 +141,26 @@ Individual validation matchers for granular testing:
|
|
134
141
|
#### Execution State Matchers
|
135
142
|
|
136
143
|
```ruby
|
137
|
-
#
|
144
|
+
# Auto-generated from CMDx::Result::STATES
|
138
145
|
expect(result).to be_initialized
|
139
146
|
expect(result).to be_executing
|
140
147
|
expect(result).to be_complete
|
141
148
|
expect(result).to be_interrupted
|
142
|
-
|
143
|
-
# Negated usage
|
144
|
-
expect(result).not_to be_initialized
|
145
149
|
```
|
146
150
|
|
151
|
+
> [!IMPORTANT]
|
152
|
+
> State matchers are dynamically generated from the CMDx framework's state definitions, ensuring they stay in sync with framework updates.
|
153
|
+
|
147
154
|
#### Execution Status Matchers
|
148
155
|
|
149
156
|
```ruby
|
150
|
-
#
|
157
|
+
# Auto-generated from CMDx::Result::STATUSES
|
151
158
|
expect(result).to be_success
|
152
159
|
expect(result).to be_skipped
|
153
160
|
expect(result).to be_failed
|
154
|
-
|
155
|
-
# Negated usage
|
156
|
-
expect(result).not_to be_success
|
157
161
|
```
|
158
162
|
|
159
|
-
|
163
|
+
### Execution and Outcome Matchers
|
160
164
|
|
161
165
|
```ruby
|
162
166
|
# Execution validation
|
@@ -164,11 +168,7 @@ expect(result).to be_executed
|
|
164
168
|
|
165
169
|
# Outcome classification
|
166
170
|
expect(result).to have_good_outcome # success OR skipped
|
167
|
-
expect(result).to have_bad_outcome # not success
|
168
|
-
|
169
|
-
# Negated usage
|
170
|
-
expect(result).not_to be_executed
|
171
|
-
expect(result).not_to have_good_outcome
|
171
|
+
expect(result).to have_bad_outcome # failed (not success)
|
172
172
|
```
|
173
173
|
|
174
174
|
### Metadata and Context Matchers
|
@@ -176,25 +176,23 @@ expect(result).not_to have_good_outcome
|
|
176
176
|
#### Metadata Validation
|
177
177
|
|
178
178
|
```ruby
|
179
|
-
# Basic metadata validation
|
180
|
-
expect(result).to have_metadata(reason: "
|
179
|
+
# Basic metadata validation
|
180
|
+
expect(result).to have_metadata(reason: "validation_failed", code: 422)
|
181
|
+
|
182
|
+
# With RSpec matchers for flexible assertions
|
181
183
|
expect(result).to have_metadata(
|
182
|
-
reason: "
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
failed_at: be_a(Time)
|
184
|
+
reason: "validation_failed",
|
185
|
+
started_at: be_a(Time),
|
186
|
+
duration: be > 0,
|
187
|
+
error_code: match(/^ERR/)
|
187
188
|
)
|
188
189
|
|
189
190
|
# Chainable metadata inclusion
|
190
|
-
expect(result).to have_metadata(reason: "
|
191
|
-
.including(
|
191
|
+
expect(result).to have_metadata(reason: "error")
|
192
|
+
.including(retry_count: 3, retryable: false)
|
192
193
|
|
193
194
|
# Empty metadata validation
|
194
195
|
expect(result).to have_empty_metadata
|
195
|
-
|
196
|
-
# Negated usage
|
197
|
-
expect(result).not_to have_metadata(reason: "Different error")
|
198
196
|
```
|
199
197
|
|
200
198
|
#### Runtime Validation
|
@@ -209,51 +207,43 @@ expect(result).to have_runtime(0.5)
|
|
209
207
|
# Runtime with RSpec matchers
|
210
208
|
expect(result).to have_runtime(be > 0)
|
211
209
|
expect(result).to have_runtime(be_within(0.1).of(0.5))
|
212
|
-
|
213
|
-
# Negated usage
|
214
|
-
expect(result).not_to have_runtime
|
210
|
+
expect(result).to have_runtime(be < 2.0) # Performance constraint
|
215
211
|
```
|
216
212
|
|
217
213
|
#### Context Side Effects
|
218
214
|
|
219
215
|
```ruby
|
220
|
-
# Context validation with
|
216
|
+
# Context validation with direct values
|
221
217
|
expect(result).to have_context(processed: true, user_id: 123)
|
222
|
-
expect(result).to have_context(
|
223
|
-
processed_at: be_a(Time),
|
224
|
-
errors: be_empty,
|
225
|
-
count: be > 0
|
226
|
-
)
|
227
218
|
|
228
|
-
#
|
219
|
+
# With RSpec matchers for flexible validation
|
229
220
|
expect(result).to have_context(
|
230
221
|
user: have_attributes(id: 123, name: "John"),
|
222
|
+
processed_at: be_a(Time),
|
231
223
|
notifications: contain_exactly("email", "sms")
|
232
224
|
)
|
233
225
|
|
234
|
-
# Context preservation
|
235
|
-
expect(result).to
|
236
|
-
|
237
|
-
|
238
|
-
|
226
|
+
# Context preservation testing
|
227
|
+
expect(result).to have_preserved_context(
|
228
|
+
user_id: 123,
|
229
|
+
original_data: "important"
|
230
|
+
)
|
239
231
|
```
|
240
232
|
|
233
|
+
> [!TIP]
|
234
|
+
> Use `have_context` for testing side effects and new values, and `have_preserved_context` for verifying that certain values remained unchanged throughout execution.
|
235
|
+
|
241
236
|
#### Chain Validation
|
242
237
|
|
243
238
|
```ruby
|
244
|
-
# Basic chain membership validation
|
245
|
-
expect(result).to belong_to_chain
|
246
|
-
|
247
|
-
# Specific chain validation
|
248
|
-
expect(result).to belong_to_chain(my_chain)
|
249
|
-
|
250
239
|
# Chain position validation
|
251
240
|
expect(result).to have_chain_index(0) # First task in chain
|
252
241
|
expect(result).to have_chain_index(2) # Third task in chain
|
253
242
|
|
254
|
-
#
|
255
|
-
|
256
|
-
|
243
|
+
# Workflow structure testing
|
244
|
+
workflow_result = MyWorkflow.call(data: "test")
|
245
|
+
first_task = workflow_result.chain.first
|
246
|
+
expect(first_task).to have_chain_index(0)
|
257
247
|
```
|
258
248
|
|
259
249
|
### Failure Chain Matchers
|
@@ -266,8 +256,10 @@ Test CMDx's failure propagation patterns:
|
|
266
256
|
# Test that result represents an original failure (not propagated)
|
267
257
|
expect(result).to have_caused_failure
|
268
258
|
|
269
|
-
#
|
270
|
-
|
259
|
+
# Distinguished from thrown failures
|
260
|
+
result = ValidateDataTask.call(data: "invalid")
|
261
|
+
expect(result).to have_caused_failure
|
262
|
+
expect(result).not_to have_thrown_failure
|
271
263
|
```
|
272
264
|
|
273
265
|
#### Failure Propagation Validation
|
@@ -277,10 +269,10 @@ expect(result).not_to have_caused_failure
|
|
277
269
|
expect(result).to have_thrown_failure
|
278
270
|
|
279
271
|
# Thrown failure with specific original result
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
expect(
|
272
|
+
workflow_result = MultiStepWorkflow.call(data: "problematic")
|
273
|
+
original_failure = workflow_result.chain.find(&:caused_failure?)
|
274
|
+
throwing_task = workflow_result.chain.find(&:threw_failure?)
|
275
|
+
expect(throwing_task).to have_thrown_failure(original_failure)
|
284
276
|
```
|
285
277
|
|
286
278
|
#### Received Failure Validation
|
@@ -289,68 +281,25 @@ expect(result).not_to have_thrown_failure
|
|
289
281
|
# Test that result received a thrown failure
|
290
282
|
expect(result).to have_received_thrown_failure
|
291
283
|
|
292
|
-
#
|
293
|
-
|
284
|
+
# Testing downstream task failure handling
|
285
|
+
workflow_result = ProcessingWorkflow.call(data: "invalid")
|
286
|
+
receiving_task = workflow_result.chain.find { |r| r.thrown_failure? }
|
287
|
+
expect(receiving_task).to have_received_thrown_failure
|
294
288
|
```
|
295
289
|
|
296
290
|
## Task Matchers
|
297
291
|
|
298
|
-
###
|
299
|
-
|
300
|
-
Test task parameter validation behavior:
|
301
|
-
|
302
|
-
#### Required Parameter Validation
|
303
|
-
|
304
|
-
```ruby
|
305
|
-
# Test that task validates required parameters
|
306
|
-
expect(CreateUserTask).to validate_required_parameter(:email)
|
307
|
-
expect(ProcessOrderTask).to validate_required_parameter(:order_id)
|
308
|
-
|
309
|
-
# Negated usage
|
310
|
-
expect(OptionalTask).not_to validate_required_parameter(:optional_field)
|
311
|
-
```
|
312
|
-
|
313
|
-
**How it works:** Calls the task without the parameter and ensures it fails with appropriate validation message.
|
314
|
-
|
315
|
-
#### Type Validation
|
316
|
-
|
317
|
-
```ruby
|
318
|
-
# Test parameter type coercion validation
|
319
|
-
expect(CreateUserTask).to validate_parameter_type(:age, :integer)
|
320
|
-
expect(UpdateSettingsTask).to validate_parameter_type(:enabled, :boolean)
|
321
|
-
expect(SearchTask).to validate_parameter_type(:filters, :hash)
|
322
|
-
|
323
|
-
# Negated usage
|
324
|
-
expect(FlexibleTask).not_to validate_parameter_type(:flexible_param, :string)
|
325
|
-
```
|
326
|
-
|
327
|
-
**How it works:** Passes invalid type values and ensures task fails with type validation message.
|
328
|
-
|
329
|
-
#### Default Value Testing
|
330
|
-
|
331
|
-
```ruby
|
332
|
-
# Test parameter default values
|
333
|
-
expect(ProcessTask).to use_default_value(:timeout, 30)
|
334
|
-
expect(EmailTask).to use_default_value(:priority, "normal")
|
335
|
-
expect(ConfigTask).to use_default_value(:enabled, true)
|
336
|
-
|
337
|
-
# Negated usage
|
338
|
-
expect(RequiredParamTask).not_to use_default_value(:required_field, nil)
|
339
|
-
```
|
340
|
-
|
341
|
-
**How it works:** Calls task without the parameter and verifies the expected default value appears in context.
|
342
|
-
|
343
|
-
### Lifecycle and Structure Matchers
|
292
|
+
### Structure and Lifecycle Matchers
|
344
293
|
|
345
294
|
#### Well-Formed Task Validation
|
346
295
|
|
347
296
|
```ruby
|
348
297
|
# Test task meets all structural requirements
|
349
298
|
expect(MyTask).to be_well_formed_task
|
350
|
-
expect(UserCreationTask).to be_well_formed_task
|
351
299
|
|
352
|
-
#
|
353
|
-
|
300
|
+
# For dynamically created tasks
|
301
|
+
task_class = Class.new(CMDx::Task) { def call; end }
|
302
|
+
expect(task_class).to be_well_formed_task
|
354
303
|
```
|
355
304
|
|
356
305
|
**What it validates:**
|
@@ -358,65 +307,60 @@ expect(BrokenTask).not_to be_well_formed_task
|
|
358
307
|
- Implements required call method
|
359
308
|
- Has properly initialized parameter, callback, and middleware registries
|
360
309
|
|
361
|
-
###
|
362
|
-
|
363
|
-
#### Graceful Exception Handling
|
364
|
-
|
365
|
-
```ruby
|
366
|
-
# Test task converts exceptions to failed results
|
367
|
-
expect(RobustTask).to handle_exceptions_gracefully
|
368
|
-
|
369
|
-
# Negated usage (for exception-propagating tasks)
|
370
|
-
expect(StrictTask).not_to handle_exceptions_gracefully
|
371
|
-
```
|
372
|
-
|
373
|
-
**How it works:** Injects exception-raising logic and verifies exceptions are caught and converted to failed results.
|
310
|
+
### Parameter Testing Matchers
|
374
311
|
|
375
|
-
####
|
312
|
+
#### Parameter Presence and Configuration
|
376
313
|
|
377
314
|
```ruby
|
378
|
-
#
|
379
|
-
expect(
|
380
|
-
|
381
|
-
#
|
382
|
-
expect(
|
315
|
+
# Basic parameter presence
|
316
|
+
expect(CreateUserTask).to have_parameter(:email)
|
317
|
+
|
318
|
+
# Parameter requirement validation
|
319
|
+
expect(ProcessOrderTask).to have_parameter(:order_id).that_is_required
|
320
|
+
expect(ConfigTask).to have_parameter(:timeout).that_is_optional
|
321
|
+
|
322
|
+
# Type coercion validation
|
323
|
+
expect(CreateUserTask).to have_parameter(:age).with_type(:integer)
|
324
|
+
expect(UpdateSettingsTask).to have_parameter(:enabled).with_coercion(:boolean)
|
325
|
+
|
326
|
+
# Default value testing
|
327
|
+
expect(ProcessTask).to have_parameter(:timeout).with_default(30)
|
328
|
+
expect(EmailTask).to have_parameter(:priority).with_default("normal")
|
329
|
+
|
330
|
+
# Validation rules testing
|
331
|
+
expect(UserTask).to have_parameter(:email)
|
332
|
+
.with_validations(:format, :presence)
|
333
|
+
.that_is_required
|
334
|
+
.with_type(:string)
|
383
335
|
```
|
384
336
|
|
385
|
-
|
337
|
+
> [!WARNING]
|
338
|
+
> Parameter validation matchers test the configuration of parameters, not their runtime behavior. Use result matchers to test parameter validation failures during execution.
|
386
339
|
|
387
340
|
### Callback and Middleware Matchers
|
388
341
|
|
389
342
|
#### Callback Registration Testing
|
390
343
|
|
391
344
|
```ruby
|
392
|
-
#
|
345
|
+
# Basic callback registration
|
393
346
|
expect(ValidatedTask).to have_callback(:before_validation)
|
394
347
|
expect(NotifiedTask).to have_callback(:on_success)
|
395
348
|
expect(CleanupTask).to have_callback(:after_execution)
|
396
349
|
|
397
|
-
#
|
350
|
+
# Callback with specific callable (if supported by implementation)
|
398
351
|
expect(CustomTask).to have_callback(:on_failure).with_callable(my_proc)
|
399
|
-
|
400
|
-
# Negated usage
|
401
|
-
expect(SimpleTask).not_to have_callback(:complex_callback)
|
402
352
|
```
|
403
353
|
|
404
354
|
#### Callback Execution Testing
|
405
355
|
|
406
356
|
```ruby
|
407
357
|
# Test callbacks execute during task lifecycle
|
408
|
-
expect(
|
409
|
-
expect(
|
410
|
-
|
411
|
-
# Single callback execution
|
412
|
-
expect(simple_task).to execute_callbacks(:on_success)
|
413
|
-
|
414
|
-
# Negated usage
|
415
|
-
expect(task).not_to execute_callbacks(:unused_callback)
|
358
|
+
expect(task_instance).to have_executed_callbacks(:before_validation, :after_validation)
|
359
|
+
expect(failed_task_instance).to have_executed_callbacks(:before_execution, :on_failure)
|
416
360
|
```
|
417
361
|
|
418
362
|
> [!NOTE]
|
419
|
-
> Callback execution testing may require mocking internal callback mechanisms for comprehensive validation.
|
363
|
+
> Callback execution testing requires task instances rather than task classes and may require mocking internal callback mechanisms for comprehensive validation.
|
420
364
|
|
421
365
|
#### Middleware Registration Testing
|
422
366
|
|
@@ -425,9 +369,6 @@ expect(task).not_to execute_callbacks(:unused_callback)
|
|
425
369
|
expect(AuthenticatedTask).to have_middleware(AuthenticationMiddleware)
|
426
370
|
expect(LoggedTask).to have_middleware(LoggingMiddleware)
|
427
371
|
expect(TimedTask).to have_middleware(TimeoutMiddleware)
|
428
|
-
|
429
|
-
# Negated usage
|
430
|
-
expect(SimpleTask).not_to have_middleware(ComplexMiddleware)
|
431
372
|
```
|
432
373
|
|
433
374
|
### Configuration Matchers
|
@@ -442,9 +383,6 @@ expect(CustomTask).to have_cmd_setting(:priority)
|
|
442
383
|
# Test setting with specific value
|
443
384
|
expect(TimedTask).to have_cmd_setting(:timeout, 30)
|
444
385
|
expect(PriorityTask).to have_cmd_setting(:priority, "high")
|
445
|
-
|
446
|
-
# Negated usage
|
447
|
-
expect(SimpleTask).not_to have_cmd_setting(:complex_setting)
|
448
386
|
```
|
449
387
|
|
450
388
|
## Composable Testing
|
@@ -458,13 +396,12 @@ Following RSpec best practices, CMDx matchers are designed for composition:
|
|
458
396
|
expect(result).to be_successful_task(user_id: 123)
|
459
397
|
.and have_context(processed_at: be_a(Time))
|
460
398
|
.and have_runtime(be > 0)
|
461
|
-
.and
|
399
|
+
.and have_chain_index(0)
|
462
400
|
|
463
401
|
# Chain task validation expectations
|
464
402
|
expect(TaskClass).to be_well_formed_task
|
465
|
-
.and
|
403
|
+
.and have_parameter(:user_id).that_is_required
|
466
404
|
.and have_callback(:before_execution)
|
467
|
-
.and handle_exceptions_gracefully
|
468
405
|
```
|
469
406
|
|
470
407
|
### Integration with Built-in RSpec Matchers
|
@@ -472,10 +409,10 @@ expect(TaskClass).to be_well_formed_task
|
|
472
409
|
```ruby
|
473
410
|
# Combine with built-in matchers
|
474
411
|
expect(result).to be_failed_task
|
475
|
-
.with_metadata(error_code:
|
412
|
+
.with_metadata(error_code: match(/^ERR/), retryable: be_falsy)
|
476
413
|
.and have_caused_failure
|
477
414
|
|
478
|
-
#
|
415
|
+
# Complex context validation
|
479
416
|
expect(result).to be_successful_task
|
480
417
|
.and have_context(
|
481
418
|
user: have_attributes(id: be_a(Integer), email: match(/@/)),
|
@@ -484,6 +421,41 @@ expect(result).to be_successful_task
|
|
484
421
|
)
|
485
422
|
```
|
486
423
|
|
424
|
+
## Error Handling
|
425
|
+
|
426
|
+
### Invalid Matcher Usage
|
427
|
+
|
428
|
+
Common error scenarios and their resolution:
|
429
|
+
|
430
|
+
```ruby
|
431
|
+
# Parameter not found
|
432
|
+
expect(SimpleTask).to have_parameter(:nonexistent)
|
433
|
+
# → "expected task to have parameter nonexistent, but had parameters: []"
|
434
|
+
|
435
|
+
# Middleware not registered
|
436
|
+
expect(SimpleTask).to have_middleware(ComplexMiddleware)
|
437
|
+
# → "expected task to have middleware ComplexMiddleware, but had []"
|
438
|
+
|
439
|
+
# Context mismatch
|
440
|
+
expect(result).to have_context(user_id: 999)
|
441
|
+
# → "expected context to include {user_id: 999}, but user_id: expected 999, got 123"
|
442
|
+
```
|
443
|
+
|
444
|
+
### Test Failures and Debugging
|
445
|
+
|
446
|
+
```ruby
|
447
|
+
# Use descriptive failure messages for debugging
|
448
|
+
result = ProcessDataTask.call(data: "invalid")
|
449
|
+
expect(result).to be_successful_task
|
450
|
+
# → "expected result to be successful, but was failed,
|
451
|
+
# expected result to be complete, but was interrupted"
|
452
|
+
|
453
|
+
# Combine matchers for comprehensive validation
|
454
|
+
expect(result).to be_failed_task("validation_error")
|
455
|
+
.with_metadata(field: "email", rule: "format")
|
456
|
+
# → Clear indication of what specifically failed
|
457
|
+
```
|
458
|
+
|
487
459
|
## Best Practices
|
488
460
|
|
489
461
|
### 1. Use Composite Matchers When Possible
|
@@ -495,7 +467,6 @@ expect(result).to be_successful_task(user_id: 123)
|
|
495
467
|
|
496
468
|
**Instead of:**
|
497
469
|
```ruby
|
498
|
-
expect(result).to be_a(CMDx::Result)
|
499
470
|
expect(result).to be_success
|
500
471
|
expect(result).to be_complete
|
501
472
|
expect(result).to be_executed
|
@@ -510,7 +481,7 @@ Use composite matchers for primary assertions, granular matchers for specific ed
|
|
510
481
|
# Primary assertion
|
511
482
|
expect(result).to be_successful_task
|
512
483
|
|
513
|
-
# Specific
|
484
|
+
# Specific validations
|
514
485
|
expect(result).to have_runtime(be < 1.0) # Performance requirement
|
515
486
|
expect(result).to have_chain_index(0) # Position validation
|
516
487
|
```
|
@@ -533,12 +504,12 @@ Matcher names are designed to read naturally in test descriptions:
|
|
533
504
|
|
534
505
|
```ruby
|
535
506
|
describe ProcessOrderTask do
|
536
|
-
it "
|
537
|
-
expect(described_class).to
|
507
|
+
it "has required parameters configured" do
|
508
|
+
expect(described_class).to have_parameter(:order_id).that_is_required
|
538
509
|
end
|
539
510
|
|
540
|
-
it "
|
541
|
-
expect(described_class).to
|
511
|
+
it "registers necessary callbacks" do
|
512
|
+
expect(described_class).to have_callback(:before_execution)
|
542
513
|
end
|
543
514
|
|
544
515
|
context "when processing succeeds" do
|
@@ -550,10 +521,33 @@ describe ProcessOrderTask do
|
|
550
521
|
.and have_runtime(be_positive)
|
551
522
|
end
|
552
523
|
end
|
524
|
+
|
525
|
+
context "when validation fails" do
|
526
|
+
it "returns failed result with error details" do
|
527
|
+
result = described_class.call(order_id: nil)
|
528
|
+
|
529
|
+
expect(result).to be_failed_task("validation_failed")
|
530
|
+
.with_metadata(field: "order_id", rule: "presence")
|
531
|
+
end
|
532
|
+
end
|
553
533
|
end
|
554
534
|
```
|
555
535
|
|
536
|
+
### 5. Test Both Happy and Error Paths
|
537
|
+
|
538
|
+
```ruby
|
539
|
+
# Happy path
|
540
|
+
expect(result).to be_successful_task
|
541
|
+
.and have_good_outcome
|
542
|
+
.and have_empty_metadata
|
543
|
+
|
544
|
+
# Error path
|
545
|
+
expect(error_result).to be_failed_task
|
546
|
+
.and have_bad_outcome
|
547
|
+
.and have_metadata(error_code: be_present)
|
548
|
+
```
|
549
|
+
|
556
550
|
---
|
557
551
|
|
558
552
|
- **Prev:** [Internationalization (i18n)](internationalization.md)
|
559
|
-
- **Next:** [
|
553
|
+
- **Next:** [Deprecation](deprecation.md)
|