cmdx 0.5.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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.cursor/rules/cursor-instructions.mdc +6 -0
  4. data/.rubocop.yml +16 -1
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +31 -1
  7. data/README.md +72 -25
  8. data/docs/ai_prompts.md +309 -0
  9. data/docs/basics/call.md +225 -14
  10. data/docs/basics/chain.md +271 -0
  11. data/docs/basics/context.md +232 -33
  12. data/docs/basics/setup.md +76 -12
  13. data/docs/callbacks.md +273 -0
  14. data/docs/configuration.md +158 -28
  15. data/docs/getting_started.md +134 -22
  16. data/docs/interruptions/exceptions.md +189 -11
  17. data/docs/interruptions/faults.md +187 -44
  18. data/docs/interruptions/halt.md +179 -35
  19. data/docs/logging.md +194 -53
  20. data/docs/middlewares.md +735 -0
  21. data/docs/outcomes/result.md +296 -10
  22. data/docs/outcomes/states.md +203 -31
  23. data/docs/outcomes/statuses.md +275 -30
  24. data/docs/parameters/coercions.md +402 -29
  25. data/docs/parameters/defaults.md +249 -25
  26. data/docs/parameters/definitions.md +238 -72
  27. data/docs/parameters/namespacing.md +250 -27
  28. data/docs/parameters/validations.md +193 -168
  29. data/docs/testing.md +550 -0
  30. data/docs/tips_and_tricks.md +95 -43
  31. data/docs/workflows.md +319 -0
  32. data/lib/cmdx/.DS_Store +0 -0
  33. data/lib/cmdx/callback.rb +69 -0
  34. data/lib/cmdx/callback_registry.rb +106 -0
  35. data/lib/cmdx/chain.rb +190 -0
  36. data/lib/cmdx/chain_inspector.rb +149 -0
  37. data/lib/cmdx/chain_serializer.rb +175 -0
  38. data/lib/cmdx/coercions/array.rb +37 -0
  39. data/lib/cmdx/coercions/big_decimal.rb +33 -0
  40. data/lib/cmdx/coercions/boolean.rb +41 -1
  41. data/lib/cmdx/coercions/complex.rb +31 -0
  42. data/lib/cmdx/coercions/date.rb +39 -0
  43. data/lib/cmdx/coercions/date_time.rb +39 -0
  44. data/lib/cmdx/coercions/float.rb +31 -0
  45. data/lib/cmdx/coercions/hash.rb +42 -0
  46. data/lib/cmdx/coercions/integer.rb +32 -0
  47. data/lib/cmdx/coercions/rational.rb +31 -0
  48. data/lib/cmdx/coercions/string.rb +31 -0
  49. data/lib/cmdx/coercions/time.rb +39 -0
  50. data/lib/cmdx/coercions/virtual.rb +31 -0
  51. data/lib/cmdx/configuration.rb +217 -9
  52. data/lib/cmdx/context.rb +173 -2
  53. data/lib/cmdx/core_ext/hash.rb +72 -0
  54. data/lib/cmdx/core_ext/module.rb +94 -0
  55. data/lib/cmdx/core_ext/object.rb +105 -0
  56. data/lib/cmdx/correlator.rb +217 -0
  57. data/lib/cmdx/error.rb +210 -8
  58. data/lib/cmdx/errors.rb +256 -1
  59. data/lib/cmdx/fault.rb +177 -2
  60. data/lib/cmdx/faults.rb +158 -2
  61. data/lib/cmdx/immutator.rb +121 -2
  62. data/lib/cmdx/lazy_struct.rb +261 -18
  63. data/lib/cmdx/log_formatters/json.rb +46 -0
  64. data/lib/cmdx/log_formatters/key_value.rb +46 -0
  65. data/lib/cmdx/log_formatters/line.rb +54 -0
  66. data/lib/cmdx/log_formatters/logstash.rb +64 -0
  67. data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
  68. data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
  69. data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
  70. data/lib/cmdx/log_formatters/raw.rb +54 -0
  71. data/lib/cmdx/logger.rb +85 -0
  72. data/lib/cmdx/logger_ansi.rb +93 -7
  73. data/lib/cmdx/logger_serializer.rb +116 -0
  74. data/lib/cmdx/middleware.rb +74 -0
  75. data/lib/cmdx/middleware_registry.rb +106 -0
  76. data/lib/cmdx/middlewares/correlate.rb +266 -0
  77. data/lib/cmdx/middlewares/timeout.rb +232 -0
  78. data/lib/cmdx/parameter.rb +228 -1
  79. data/lib/cmdx/parameter_inspector.rb +61 -0
  80. data/lib/cmdx/parameter_registry.rb +125 -0
  81. data/lib/cmdx/parameter_serializer.rb +83 -0
  82. data/lib/cmdx/parameter_validator.rb +62 -0
  83. data/lib/cmdx/parameter_value.rb +109 -1
  84. data/lib/cmdx/parameters_inspector.rb +59 -0
  85. data/lib/cmdx/parameters_serializer.rb +102 -0
  86. data/lib/cmdx/railtie.rb +123 -3
  87. data/lib/cmdx/result.rb +367 -25
  88. data/lib/cmdx/result_ansi.rb +105 -9
  89. data/lib/cmdx/result_inspector.rb +76 -0
  90. data/lib/cmdx/result_logger.rb +90 -3
  91. data/lib/cmdx/result_serializer.rb +137 -0
  92. data/lib/cmdx/rspec/result_matchers.rb +917 -0
  93. data/lib/cmdx/rspec/task_matchers.rb +570 -0
  94. data/lib/cmdx/task.rb +405 -37
  95. data/lib/cmdx/task_serializer.rb +74 -2
  96. data/lib/cmdx/utils/ansi_color.rb +95 -0
  97. data/lib/cmdx/utils/log_timestamp.rb +48 -0
  98. data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
  99. data/lib/cmdx/utils/name_affix.rb +78 -0
  100. data/lib/cmdx/validators/custom.rb +82 -0
  101. data/lib/cmdx/validators/exclusion.rb +94 -0
  102. data/lib/cmdx/validators/format.rb +102 -8
  103. data/lib/cmdx/validators/inclusion.rb +104 -0
  104. data/lib/cmdx/validators/length.rb +128 -0
  105. data/lib/cmdx/validators/numeric.rb +128 -0
  106. data/lib/cmdx/validators/presence.rb +93 -7
  107. data/lib/cmdx/version.rb +7 -1
  108. data/lib/cmdx/workflow.rb +394 -0
  109. data/lib/cmdx.rb +25 -64
  110. data/lib/generators/cmdx/install_generator.rb +37 -1
  111. data/lib/generators/cmdx/task_generator.rb +69 -1
  112. data/lib/generators/cmdx/templates/install.rb +8 -12
  113. data/lib/generators/cmdx/workflow_generator.rb +109 -0
  114. metadata +54 -15
  115. data/docs/basics/run.md +0 -34
  116. data/docs/batch.md +0 -53
  117. data/docs/example.md +0 -82
  118. data/docs/hooks.md +0 -62
  119. data/lib/cmdx/batch.rb +0 -43
  120. data/lib/cmdx/parameters.rb +0 -35
  121. data/lib/cmdx/run.rb +0 -39
  122. data/lib/cmdx/run_inspector.rb +0 -26
  123. data/lib/cmdx/run_serializer.rb +0 -20
  124. data/lib/cmdx/task_hook.rb +0 -18
  125. data/lib/generators/cmdx/batch_generator.rb +0 -30
  126. /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)