cmdx 0.4.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/.cursor/rules/cursor-instructions.mdc +6 -0
- data/.rubocop.yml +16 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +42 -1
- data/README.md +72 -25
- data/docs/ai_prompts.md +309 -0
- data/docs/basics/call.md +225 -14
- data/docs/basics/chain.md +271 -0
- data/docs/basics/context.md +232 -33
- data/docs/basics/setup.md +76 -12
- data/docs/callbacks.md +273 -0
- data/docs/configuration.md +158 -28
- data/docs/getting_started.md +134 -22
- data/docs/interruptions/exceptions.md +189 -11
- data/docs/interruptions/faults.md +187 -44
- data/docs/interruptions/halt.md +179 -35
- data/docs/logging.md +194 -53
- data/docs/middlewares.md +735 -0
- data/docs/outcomes/result.md +296 -10
- data/docs/outcomes/states.md +212 -19
- data/docs/outcomes/statuses.md +284 -18
- data/docs/parameters/coercions.md +402 -29
- data/docs/parameters/defaults.md +249 -25
- data/docs/parameters/definitions.md +238 -72
- data/docs/parameters/namespacing.md +250 -27
- data/docs/parameters/validations.md +193 -168
- data/docs/testing.md +550 -0
- data/docs/tips_and_tricks.md +95 -43
- data/docs/workflows.md +319 -0
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +69 -0
- data/lib/cmdx/callback_registry.rb +106 -0
- data/lib/cmdx/chain.rb +190 -0
- data/lib/cmdx/chain_inspector.rb +149 -0
- data/lib/cmdx/chain_serializer.rb +175 -0
- data/lib/cmdx/coercions/array.rb +37 -0
- data/lib/cmdx/coercions/big_decimal.rb +33 -0
- data/lib/cmdx/coercions/boolean.rb +41 -1
- data/lib/cmdx/coercions/complex.rb +31 -0
- data/lib/cmdx/coercions/date.rb +39 -0
- data/lib/cmdx/coercions/date_time.rb +39 -0
- data/lib/cmdx/coercions/float.rb +31 -0
- data/lib/cmdx/coercions/hash.rb +42 -0
- data/lib/cmdx/coercions/integer.rb +32 -0
- data/lib/cmdx/coercions/rational.rb +31 -0
- data/lib/cmdx/coercions/string.rb +31 -0
- data/lib/cmdx/coercions/time.rb +39 -0
- data/lib/cmdx/coercions/virtual.rb +31 -0
- data/lib/cmdx/configuration.rb +217 -9
- data/lib/cmdx/context.rb +173 -2
- data/lib/cmdx/core_ext/hash.rb +72 -0
- data/lib/cmdx/core_ext/module.rb +94 -0
- data/lib/cmdx/core_ext/object.rb +105 -0
- data/lib/cmdx/correlator.rb +217 -0
- data/lib/cmdx/error.rb +210 -8
- data/lib/cmdx/errors.rb +256 -1
- data/lib/cmdx/fault.rb +177 -2
- data/lib/cmdx/faults.rb +158 -2
- data/lib/cmdx/immutator.rb +121 -2
- data/lib/cmdx/lazy_struct.rb +261 -18
- data/lib/cmdx/log_formatters/json.rb +46 -0
- data/lib/cmdx/log_formatters/key_value.rb +46 -0
- data/lib/cmdx/log_formatters/line.rb +54 -0
- data/lib/cmdx/log_formatters/logstash.rb +64 -0
- data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
- data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
- data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
- data/lib/cmdx/log_formatters/raw.rb +54 -0
- data/lib/cmdx/logger.rb +85 -0
- data/lib/cmdx/logger_ansi.rb +93 -7
- data/lib/cmdx/logger_serializer.rb +116 -0
- data/lib/cmdx/middleware.rb +74 -0
- data/lib/cmdx/middleware_registry.rb +106 -0
- data/lib/cmdx/middlewares/correlate.rb +266 -0
- data/lib/cmdx/middlewares/timeout.rb +232 -0
- data/lib/cmdx/parameter.rb +228 -1
- data/lib/cmdx/parameter_inspector.rb +61 -0
- data/lib/cmdx/parameter_registry.rb +125 -0
- data/lib/cmdx/parameter_serializer.rb +83 -0
- data/lib/cmdx/parameter_validator.rb +62 -0
- data/lib/cmdx/parameter_value.rb +109 -1
- data/lib/cmdx/parameters_inspector.rb +59 -0
- data/lib/cmdx/parameters_serializer.rb +102 -0
- data/lib/cmdx/railtie.rb +123 -3
- data/lib/cmdx/result.rb +399 -20
- data/lib/cmdx/result_ansi.rb +105 -9
- data/lib/cmdx/result_inspector.rb +76 -0
- data/lib/cmdx/result_logger.rb +90 -3
- data/lib/cmdx/result_serializer.rb +137 -0
- data/lib/cmdx/rspec/result_matchers.rb +917 -0
- data/lib/cmdx/rspec/task_matchers.rb +570 -0
- data/lib/cmdx/task.rb +409 -34
- data/lib/cmdx/task_serializer.rb +74 -2
- data/lib/cmdx/utils/ansi_color.rb +95 -0
- data/lib/cmdx/utils/log_timestamp.rb +48 -0
- data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
- data/lib/cmdx/utils/name_affix.rb +78 -0
- data/lib/cmdx/validators/custom.rb +82 -0
- data/lib/cmdx/validators/exclusion.rb +94 -0
- data/lib/cmdx/validators/format.rb +102 -8
- data/lib/cmdx/validators/inclusion.rb +104 -0
- data/lib/cmdx/validators/length.rb +128 -0
- data/lib/cmdx/validators/numeric.rb +128 -0
- data/lib/cmdx/validators/presence.rb +93 -7
- data/lib/cmdx/version.rb +7 -1
- data/lib/cmdx/workflow.rb +394 -0
- data/lib/cmdx.rb +25 -64
- data/lib/generators/cmdx/install_generator.rb +37 -1
- data/lib/generators/cmdx/task_generator.rb +69 -1
- data/lib/generators/cmdx/templates/install.rb +8 -12
- data/lib/generators/cmdx/workflow_generator.rb +109 -0
- metadata +54 -15
- data/docs/basics/run.md +0 -34
- data/docs/batch.md +0 -53
- data/docs/example.md +0 -82
- data/docs/hooks.md +0 -59
- data/lib/cmdx/batch.rb +0 -43
- data/lib/cmdx/parameters.rb +0 -34
- data/lib/cmdx/run.rb +0 -38
- data/lib/cmdx/run_inspector.rb +0 -26
- data/lib/cmdx/run_serializer.rb +0 -16
- data/lib/cmdx/task_hook.rb +0 -18
- data/lib/generators/cmdx/batch_generator.rb +0 -30
- /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
data/docs/testing.md
ADDED
@@ -0,0 +1,550 @@
|
|
1
|
+
# Testing
|
2
|
+
|
3
|
+
CMDx provides a comprehensive suite of custom RSpec matchers designed for expressive, maintainable testing of tasks, results, and business logic workflows.
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
|
7
|
+
- [External Project Setup](#external-project-setup)
|
8
|
+
- [Matcher Organization](#matcher-organization)
|
9
|
+
- [Result Matchers](#result-matchers)
|
10
|
+
- [Primary Outcome Matchers](#primary-outcome-matchers)
|
11
|
+
- [State and Status Matchers](#state-and-status-matchers)
|
12
|
+
- [Metadata and Context Matchers](#metadata-and-context-matchers)
|
13
|
+
- [Failure Chain Matchers](#failure-chain-matchers)
|
14
|
+
- [Task Matchers](#task-matchers)
|
15
|
+
- [Parameter Validation Matchers](#parameter-validation-matchers)
|
16
|
+
- [Lifecycle and Structure Matchers](#lifecycle-and-structure-matchers)
|
17
|
+
- [Exception Handling Matchers](#exception-handling-matchers)
|
18
|
+
- [Callback and Middleware Matchers](#callback-and-middleware-matchers)
|
19
|
+
- [Configuration Matchers](#configuration-matchers)
|
20
|
+
- [Composable Testing](#composable-testing)
|
21
|
+
- [Best Practices](#best-practices)
|
22
|
+
|
23
|
+
## Using RSpec matchers
|
24
|
+
|
25
|
+
To use CMDx's custom matchers in an external RSpec-based project update your `spec/spec_helper.rb` or `spec/rails_helper.rb`:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
require "cmdx/rspec/result_matchers"
|
29
|
+
require "cmdx/rspec/task_matchers"
|
30
|
+
```
|
31
|
+
|
32
|
+
## Matcher Organization
|
33
|
+
|
34
|
+
CMDx matchers are organized into two primary files with comprehensive YARD documentation:
|
35
|
+
|
36
|
+
| File | Purpose | Matcher Count |
|
37
|
+
|------|---------|---------------|
|
38
|
+
| `result_matchers.rb` | Task execution outcomes and side effects | 25+ matchers |
|
39
|
+
| `task_matchers.rb` | Task behavior, validation, and lifecycle | 15+ matchers |
|
40
|
+
|
41
|
+
All matchers include:
|
42
|
+
- Complete parameter descriptions
|
43
|
+
- Multiple usage examples
|
44
|
+
- Return value specifications
|
45
|
+
- Negation examples
|
46
|
+
- Version information
|
47
|
+
|
48
|
+
## Result Matchers
|
49
|
+
|
50
|
+
### Primary Outcome Matchers
|
51
|
+
|
52
|
+
These composite matchers validate complete task execution scenarios with single assertions:
|
53
|
+
|
54
|
+
#### Successful Task Validation
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
# Basic successful task validation
|
58
|
+
expect(result).to be_successful_task
|
59
|
+
|
60
|
+
# Successful task with context validation
|
61
|
+
expect(result).to be_successful_task(user_id: 123, processed: true)
|
62
|
+
|
63
|
+
# Negated usage
|
64
|
+
expect(result).not_to be_successful_task
|
65
|
+
```
|
66
|
+
|
67
|
+
**What it validates:**
|
68
|
+
- Result has success status
|
69
|
+
- Result is in complete state
|
70
|
+
- Result was executed
|
71
|
+
- Optional context attributes match
|
72
|
+
|
73
|
+
#### Failed Task Validation
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
# Basic failed task validation
|
77
|
+
expect(result).to be_failed_task
|
78
|
+
|
79
|
+
# Failed task with specific reason
|
80
|
+
expect(result).to be_failed_task("Validation failed")
|
81
|
+
|
82
|
+
# Chainable reason and metadata validation
|
83
|
+
expect(result).to be_failed_task
|
84
|
+
.with_reason("Invalid data")
|
85
|
+
.with_metadata(error_code: "ERR001", retryable: false)
|
86
|
+
|
87
|
+
# Negated usage
|
88
|
+
expect(result).not_to be_failed_task
|
89
|
+
```
|
90
|
+
|
91
|
+
**What it validates:**
|
92
|
+
- Result has failed status
|
93
|
+
- Result is in interrupted state
|
94
|
+
- Result was executed
|
95
|
+
- Optional reason and metadata match
|
96
|
+
|
97
|
+
#### Skipped Task Validation
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
# Basic skipped task validation
|
101
|
+
expect(result).to be_skipped_task
|
102
|
+
|
103
|
+
# Skipped task with specific reason
|
104
|
+
expect(result).to be_skipped_task("Already processed")
|
105
|
+
|
106
|
+
# Chainable reason and metadata validation
|
107
|
+
expect(result).to be_skipped_task
|
108
|
+
.with_reason("Order already processed")
|
109
|
+
.with_metadata(processed_at: be_a(Time), skip_code: "DUPLICATE")
|
110
|
+
|
111
|
+
# Negated usage
|
112
|
+
expect(result).not_to be_skipped_task
|
113
|
+
```
|
114
|
+
|
115
|
+
**What it validates:**
|
116
|
+
- Result has skipped status
|
117
|
+
- Result is in interrupted state
|
118
|
+
- Result was executed
|
119
|
+
- Optional reason and metadata match
|
120
|
+
|
121
|
+
### State and Status Matchers
|
122
|
+
|
123
|
+
Individual validation matchers for granular testing:
|
124
|
+
|
125
|
+
#### Execution State Matchers
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
# Individual state checks (auto-generated from CMDx::Result::STATES)
|
129
|
+
expect(result).to be_initialized
|
130
|
+
expect(result).to be_executing
|
131
|
+
expect(result).to be_complete
|
132
|
+
expect(result).to be_interrupted
|
133
|
+
|
134
|
+
# Negated usage
|
135
|
+
expect(result).not_to be_initialized
|
136
|
+
```
|
137
|
+
|
138
|
+
#### Execution Status Matchers
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
# Individual status checks (auto-generated from CMDx::Result::STATUSES)
|
142
|
+
expect(result).to be_success
|
143
|
+
expect(result).to be_skipped
|
144
|
+
expect(result).to be_failed
|
145
|
+
|
146
|
+
# Negated usage
|
147
|
+
expect(result).not_to be_success
|
148
|
+
```
|
149
|
+
|
150
|
+
#### Execution and Outcome Matchers
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
# Execution validation
|
154
|
+
expect(result).to be_executed
|
155
|
+
|
156
|
+
# Outcome classification
|
157
|
+
expect(result).to have_good_outcome # success OR skipped
|
158
|
+
expect(result).to have_bad_outcome # not success (includes skipped and failed)
|
159
|
+
|
160
|
+
# Negated usage
|
161
|
+
expect(result).not_to be_executed
|
162
|
+
expect(result).not_to have_good_outcome
|
163
|
+
```
|
164
|
+
|
165
|
+
### Metadata and Context Matchers
|
166
|
+
|
167
|
+
#### Metadata Validation
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
# Basic metadata validation with RSpec matcher support
|
171
|
+
expect(result).to have_metadata(reason: "Error", code: "001")
|
172
|
+
expect(result).to have_metadata(
|
173
|
+
reason: "Invalid email format",
|
174
|
+
errors: ["Email must contain @"],
|
175
|
+
error_code: "VALIDATION_FAILED",
|
176
|
+
retryable: false,
|
177
|
+
failed_at: be_a(Time)
|
178
|
+
)
|
179
|
+
|
180
|
+
# Chainable metadata inclusion
|
181
|
+
expect(result).to have_metadata(reason: "Error")
|
182
|
+
.including(code: "001", retryable: false)
|
183
|
+
|
184
|
+
# Empty metadata validation
|
185
|
+
expect(result).to have_empty_metadata
|
186
|
+
|
187
|
+
# Negated usage
|
188
|
+
expect(result).not_to have_metadata(reason: "Different error")
|
189
|
+
```
|
190
|
+
|
191
|
+
#### Runtime Validation
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
# Basic runtime presence validation
|
195
|
+
expect(result).to have_runtime
|
196
|
+
|
197
|
+
# Runtime with specific value
|
198
|
+
expect(result).to have_runtime(0.5)
|
199
|
+
|
200
|
+
# Runtime with RSpec matchers
|
201
|
+
expect(result).to have_runtime(be > 0)
|
202
|
+
expect(result).to have_runtime(be_within(0.1).of(0.5))
|
203
|
+
|
204
|
+
# Negated usage
|
205
|
+
expect(result).not_to have_runtime
|
206
|
+
```
|
207
|
+
|
208
|
+
#### Context Side Effects
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
# Context validation with RSpec matcher support
|
212
|
+
expect(result).to have_context(processed: true, user_id: 123)
|
213
|
+
expect(result).to have_context(
|
214
|
+
processed_at: be_a(Time),
|
215
|
+
errors: be_empty,
|
216
|
+
count: be > 0
|
217
|
+
)
|
218
|
+
|
219
|
+
# Complex side effects validation
|
220
|
+
expect(result).to have_context(
|
221
|
+
user: have_attributes(id: 123, name: "John"),
|
222
|
+
notifications: contain_exactly("email", "sms")
|
223
|
+
)
|
224
|
+
|
225
|
+
# Context preservation
|
226
|
+
expect(result).to preserve_context(original_data)
|
227
|
+
|
228
|
+
# Negated usage
|
229
|
+
expect(result).not_to have_context(deleted: true)
|
230
|
+
```
|
231
|
+
|
232
|
+
#### Chain Validation
|
233
|
+
|
234
|
+
```ruby
|
235
|
+
# Basic chain membership validation
|
236
|
+
expect(result).to belong_to_chain
|
237
|
+
|
238
|
+
# Specific chain validation
|
239
|
+
expect(result).to belong_to_chain(my_chain)
|
240
|
+
|
241
|
+
# Chain position validation
|
242
|
+
expect(result).to have_chain_index(0) # First task in chain
|
243
|
+
expect(result).to have_chain_index(2) # Third task in chain
|
244
|
+
|
245
|
+
# Negated usage
|
246
|
+
expect(result).not_to belong_to_chain
|
247
|
+
expect(result).not_to have_chain_index(1)
|
248
|
+
```
|
249
|
+
|
250
|
+
### Failure Chain Matchers
|
251
|
+
|
252
|
+
Test CMDx's failure propagation patterns:
|
253
|
+
|
254
|
+
#### Original Failure Validation
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
# Test that result represents an original failure (not propagated)
|
258
|
+
expect(result).to have_caused_failure
|
259
|
+
|
260
|
+
# Negated usage (for thrown failures)
|
261
|
+
expect(result).not_to have_caused_failure
|
262
|
+
```
|
263
|
+
|
264
|
+
#### Failure Propagation Validation
|
265
|
+
|
266
|
+
```ruby
|
267
|
+
# Basic thrown failure validation
|
268
|
+
expect(result).to have_thrown_failure
|
269
|
+
|
270
|
+
# Thrown failure with specific original result
|
271
|
+
expect(result).to have_thrown_failure(original_failed_result)
|
272
|
+
|
273
|
+
# Negated usage (for caused failures)
|
274
|
+
expect(result).not_to have_thrown_failure
|
275
|
+
```
|
276
|
+
|
277
|
+
#### Received Failure Validation
|
278
|
+
|
279
|
+
```ruby
|
280
|
+
# Test that result received a thrown failure
|
281
|
+
expect(result).to have_received_thrown_failure
|
282
|
+
|
283
|
+
# Negated usage
|
284
|
+
expect(result).not_to have_received_thrown_failure
|
285
|
+
```
|
286
|
+
|
287
|
+
## Task Matchers
|
288
|
+
|
289
|
+
### Parameter Validation Matchers
|
290
|
+
|
291
|
+
Test task parameter validation behavior:
|
292
|
+
|
293
|
+
#### Required Parameter Validation
|
294
|
+
|
295
|
+
```ruby
|
296
|
+
# Test that task validates required parameters
|
297
|
+
expect(CreateUserTask).to validate_required_parameter(:email)
|
298
|
+
expect(ProcessOrderTask).to validate_required_parameter(:order_id)
|
299
|
+
|
300
|
+
# Negated usage
|
301
|
+
expect(OptionalTask).not_to validate_required_parameter(:optional_field)
|
302
|
+
```
|
303
|
+
|
304
|
+
**How it works:** Calls the task without the parameter and ensures it fails with appropriate validation message.
|
305
|
+
|
306
|
+
#### Type Validation
|
307
|
+
|
308
|
+
```ruby
|
309
|
+
# Test parameter type coercion validation
|
310
|
+
expect(CreateUserTask).to validate_parameter_type(:age, :integer)
|
311
|
+
expect(UpdateSettingsTask).to validate_parameter_type(:enabled, :boolean)
|
312
|
+
expect(SearchTask).to validate_parameter_type(:filters, :hash)
|
313
|
+
|
314
|
+
# Negated usage
|
315
|
+
expect(FlexibleTask).not_to validate_parameter_type(:flexible_param, :string)
|
316
|
+
```
|
317
|
+
|
318
|
+
**How it works:** Passes invalid type values and ensures task fails with type validation message.
|
319
|
+
|
320
|
+
#### Default Value Testing
|
321
|
+
|
322
|
+
```ruby
|
323
|
+
# Test parameter default values
|
324
|
+
expect(ProcessTask).to use_default_value(:timeout, 30)
|
325
|
+
expect(EmailTask).to use_default_value(:priority, "normal")
|
326
|
+
expect(ConfigTask).to use_default_value(:enabled, true)
|
327
|
+
|
328
|
+
# Negated usage
|
329
|
+
expect(RequiredParamTask).not_to use_default_value(:required_field, nil)
|
330
|
+
```
|
331
|
+
|
332
|
+
**How it works:** Calls task without the parameter and verifies the expected default value appears in context.
|
333
|
+
|
334
|
+
### Lifecycle and Structure Matchers
|
335
|
+
|
336
|
+
#### Well-Formed Task Validation
|
337
|
+
|
338
|
+
```ruby
|
339
|
+
# Test task meets all structural requirements
|
340
|
+
expect(MyTask).to be_well_formed_task
|
341
|
+
expect(UserCreationTask).to be_well_formed_task
|
342
|
+
|
343
|
+
# Negated usage (for malformed tasks)
|
344
|
+
expect(BrokenTask).not_to be_well_formed_task
|
345
|
+
```
|
346
|
+
|
347
|
+
**What it validates:**
|
348
|
+
- Inherits from CMDx::Task
|
349
|
+
- Implements required call method
|
350
|
+
- Has properly initialized parameter, callback, and middleware registries
|
351
|
+
|
352
|
+
### Exception Handling Matchers
|
353
|
+
|
354
|
+
#### Graceful Exception Handling
|
355
|
+
|
356
|
+
```ruby
|
357
|
+
# Test task converts exceptions to failed results
|
358
|
+
expect(RobustTask).to handle_exceptions_gracefully
|
359
|
+
|
360
|
+
# Negated usage (for exception-propagating tasks)
|
361
|
+
expect(StrictTask).not_to handle_exceptions_gracefully
|
362
|
+
```
|
363
|
+
|
364
|
+
**How it works:** Injects exception-raising logic and verifies exceptions are caught and converted to failed results.
|
365
|
+
|
366
|
+
#### Bang Method Exception Propagation
|
367
|
+
|
368
|
+
```ruby
|
369
|
+
# Test task propagates exceptions with call!
|
370
|
+
expect(MyTask).to propagate_exceptions_with_bang
|
371
|
+
|
372
|
+
# Negated usage (for always-graceful tasks)
|
373
|
+
expect(AlwaysGracefulTask).not_to propagate_exceptions_with_bang
|
374
|
+
```
|
375
|
+
|
376
|
+
**How it works:** Tests that `call!` method propagates exceptions instead of handling them gracefully.
|
377
|
+
|
378
|
+
### Callback and Middleware Matchers
|
379
|
+
|
380
|
+
#### Callback Registration Testing
|
381
|
+
|
382
|
+
```ruby
|
383
|
+
# Test basic callback registration
|
384
|
+
expect(ValidatedTask).to have_callback(:before_validation)
|
385
|
+
expect(NotifiedTask).to have_callback(:on_success)
|
386
|
+
expect(CleanupTask).to have_callback(:after_execution)
|
387
|
+
|
388
|
+
# Test callback with specific callable
|
389
|
+
expect(CustomTask).to have_callback(:on_failure).with_callable(my_proc)
|
390
|
+
|
391
|
+
# Negated usage
|
392
|
+
expect(SimpleTask).not_to have_callback(:complex_callback)
|
393
|
+
```
|
394
|
+
|
395
|
+
#### Callback Execution Testing
|
396
|
+
|
397
|
+
```ruby
|
398
|
+
# Test callbacks execute during task lifecycle
|
399
|
+
expect(task).to execute_callbacks(:before_validation, :after_validation)
|
400
|
+
expect(failed_task).to execute_callbacks(:before_execution, :on_failure)
|
401
|
+
|
402
|
+
# Single callback execution
|
403
|
+
expect(simple_task).to execute_callbacks(:on_success)
|
404
|
+
|
405
|
+
# Negated usage
|
406
|
+
expect(task).not_to execute_callbacks(:unused_callback)
|
407
|
+
```
|
408
|
+
|
409
|
+
> [!NOTE]
|
410
|
+
> Callback execution testing may require mocking internal callback mechanisms for comprehensive validation.
|
411
|
+
|
412
|
+
#### Middleware Registration Testing
|
413
|
+
|
414
|
+
```ruby
|
415
|
+
# Test middleware registration
|
416
|
+
expect(AuthenticatedTask).to have_middleware(AuthenticationMiddleware)
|
417
|
+
expect(LoggedTask).to have_middleware(LoggingMiddleware)
|
418
|
+
expect(TimedTask).to have_middleware(TimeoutMiddleware)
|
419
|
+
|
420
|
+
# Negated usage
|
421
|
+
expect(SimpleTask).not_to have_middleware(ComplexMiddleware)
|
422
|
+
```
|
423
|
+
|
424
|
+
### Configuration Matchers
|
425
|
+
|
426
|
+
#### Task Setting Validation
|
427
|
+
|
428
|
+
```ruby
|
429
|
+
# Test setting presence
|
430
|
+
expect(ConfiguredTask).to have_task_setting(:timeout)
|
431
|
+
expect(CustomTask).to have_task_setting(:priority)
|
432
|
+
|
433
|
+
# Test setting with specific value
|
434
|
+
expect(TimedTask).to have_task_setting(:timeout, 30)
|
435
|
+
expect(PriorityTask).to have_task_setting(:priority, "high")
|
436
|
+
|
437
|
+
# Negated usage
|
438
|
+
expect(SimpleTask).not_to have_task_setting(:complex_setting)
|
439
|
+
```
|
440
|
+
|
441
|
+
## Composable Testing
|
442
|
+
|
443
|
+
Following RSpec best practices, CMDx matchers are designed for composition:
|
444
|
+
|
445
|
+
### Chaining with `.and`
|
446
|
+
|
447
|
+
```ruby
|
448
|
+
# Chain multiple result expectations
|
449
|
+
expect(result).to be_successful_task(user_id: 123)
|
450
|
+
.and have_context(processed_at: be_a(Time))
|
451
|
+
.and have_runtime(be > 0)
|
452
|
+
.and belong_to_chain
|
453
|
+
|
454
|
+
# Chain task validation expectations
|
455
|
+
expect(TaskClass).to be_well_formed_task
|
456
|
+
.and validate_required_parameter(:user_id)
|
457
|
+
.and have_callback(:before_execution)
|
458
|
+
.and handle_exceptions_gracefully
|
459
|
+
```
|
460
|
+
|
461
|
+
### Integration with Built-in RSpec Matchers
|
462
|
+
|
463
|
+
```ruby
|
464
|
+
# Combine with built-in matchers
|
465
|
+
expect(result).to be_failed_task
|
466
|
+
.with_metadata(error_code: "ERR001", retryable: be_falsy)
|
467
|
+
.and have_caused_failure
|
468
|
+
|
469
|
+
# Use in complex scenarios
|
470
|
+
expect(result).to be_successful_task
|
471
|
+
.and have_context(
|
472
|
+
user: have_attributes(id: be_a(Integer), email: match(/@/)),
|
473
|
+
timestamps: all(be_a(Time)),
|
474
|
+
notifications: contain_exactly("email", "sms")
|
475
|
+
)
|
476
|
+
```
|
477
|
+
|
478
|
+
## Best Practices
|
479
|
+
|
480
|
+
### 1. Use Composite Matchers When Possible
|
481
|
+
|
482
|
+
**Preferred:**
|
483
|
+
```ruby
|
484
|
+
expect(result).to be_successful_task(user_id: 123)
|
485
|
+
```
|
486
|
+
|
487
|
+
**Instead of:**
|
488
|
+
```ruby
|
489
|
+
expect(result).to be_a(CMDx::Result)
|
490
|
+
expect(result).to be_success
|
491
|
+
expect(result).to be_complete
|
492
|
+
expect(result).to be_executed
|
493
|
+
expect(result.context.user_id).to eq(123)
|
494
|
+
```
|
495
|
+
|
496
|
+
### 2. Combine Granular and Composite Testing
|
497
|
+
|
498
|
+
Use composite matchers for primary assertions, granular matchers for specific edge cases:
|
499
|
+
|
500
|
+
```ruby
|
501
|
+
# Primary assertion
|
502
|
+
expect(result).to be_successful_task
|
503
|
+
|
504
|
+
# Specific edge case validation
|
505
|
+
expect(result).to have_runtime(be < 1.0) # Performance requirement
|
506
|
+
expect(result).to have_chain_index(0) # Position validation
|
507
|
+
```
|
508
|
+
|
509
|
+
### 3. Leverage RSpec Matcher Integration
|
510
|
+
|
511
|
+
CMDx matchers work seamlessly with built-in RSpec matchers:
|
512
|
+
|
513
|
+
```ruby
|
514
|
+
expect(result).to have_metadata(
|
515
|
+
timestamp: be_within(1.second).of(Time.current),
|
516
|
+
errors: be_empty,
|
517
|
+
count: be_between(1, 100)
|
518
|
+
)
|
519
|
+
```
|
520
|
+
|
521
|
+
### 4. Write Descriptive Test Names
|
522
|
+
|
523
|
+
Matcher names are designed to read naturally in test descriptions:
|
524
|
+
|
525
|
+
```ruby
|
526
|
+
describe ProcessOrderTask do
|
527
|
+
it "validates required parameters" do
|
528
|
+
expect(described_class).to validate_required_parameter(:order_id)
|
529
|
+
end
|
530
|
+
|
531
|
+
it "handles exceptions gracefully" do
|
532
|
+
expect(described_class).to handle_exceptions_gracefully
|
533
|
+
end
|
534
|
+
|
535
|
+
context "when processing succeeds" do
|
536
|
+
it "returns successful result with order data" do
|
537
|
+
result = described_class.call(order_id: 123)
|
538
|
+
|
539
|
+
expect(result).to be_successful_task(order_id: 123)
|
540
|
+
.and have_context(order: be_present, processed_at: be_a(Time))
|
541
|
+
.and have_runtime(be_positive)
|
542
|
+
end
|
543
|
+
end
|
544
|
+
end
|
545
|
+
```
|
546
|
+
|
547
|
+
---
|
548
|
+
|
549
|
+
- **Prev:** [Logging](logging.md)
|
550
|
+
- **Next:** [AI Prompts](ai_prompts.md)
|