cmdx-rspec 1.4.0 → 2.1.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/CHANGELOG.md +25 -0
- data/README.md +337 -169
- data/lib/cmdx/rspec/helpers.rb +453 -291
- data/lib/cmdx/rspec/matchers/be_complete.rb +20 -0
- data/lib/cmdx/rspec/matchers/be_deprecated.rb +14 -52
- data/lib/cmdx/rspec/matchers/be_interrupted.rb +20 -0
- data/lib/cmdx/rspec/matchers/be_ko.rb +19 -0
- data/lib/cmdx/rspec/matchers/be_ok.rb +19 -0
- data/lib/cmdx/rspec/matchers/be_successful.rb +8 -20
- data/lib/cmdx/rspec/matchers/have_been_retried.rb +41 -0
- data/lib/cmdx/rspec/matchers/have_been_rolled_back.rb +23 -0
- data/lib/cmdx/rspec/matchers/have_callback.rb +46 -0
- data/lib/cmdx/rspec/matchers/have_chain_root.rb +30 -0
- data/lib/cmdx/rspec/matchers/have_chain_size.rb +29 -0
- data/lib/cmdx/rspec/matchers/have_duration.rb +30 -0
- data/lib/cmdx/rspec/matchers/have_empty_context.rb +4 -16
- data/lib/cmdx/rspec/matchers/have_empty_metadata.rb +3 -9
- data/lib/cmdx/rspec/matchers/have_errors_on.rb +50 -0
- data/lib/cmdx/rspec/matchers/have_failed.rb +7 -24
- data/lib/cmdx/rspec/matchers/have_input.rb +41 -0
- data/lib/cmdx/rspec/matchers/have_matching_context.rb +5 -20
- data/lib/cmdx/rspec/matchers/have_matching_metadata.rb +5 -17
- data/lib/cmdx/rspec/matchers/have_middleware.rb +29 -0
- data/lib/cmdx/rspec/matchers/have_no_errors.rb +30 -0
- data/lib/cmdx/rspec/matchers/have_output.rb +46 -0
- data/lib/cmdx/rspec/matchers/have_pipeline_tasks.rb +27 -0
- data/lib/cmdx/rspec/matchers/have_retry_on.rb +36 -0
- data/lib/cmdx/rspec/matchers/have_skipped.rb +7 -24
- data/lib/cmdx/rspec/matchers/have_tag.rb +30 -0
- data/lib/cmdx/rspec/matchers/raise_cmdx_fault.rb +74 -0
- data/lib/cmdx/rspec/version.rb +2 -1
- data/lib/cmdx/rspec.rb +22 -3
- metadata +25 -5
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
Collection of RSpec matchers for the CMDx framework.
|
|
7
|
+
Collection of RSpec matchers and helpers for the CMDx framework.
|
|
8
8
|
|
|
9
9
|
[Changelog](./CHANGELOG.md) · [Report Bug](https://github.com/drexed/cmdx-rspec/issues) · [Request Feature](https://github.com/drexed/cmdx-rspec/issues)
|
|
10
10
|
|
|
@@ -15,141 +15,277 @@
|
|
|
15
15
|
|
|
16
16
|
# CMDx::RSpec
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
RSpec matchers and helpers for asserting [CMDx](https://github.com/drexed/cmdx) task and workflow behavior — result state, errors, faults, callbacks, retries, chains, and more — without invoking real `work` blocks.
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- Ruby: MRI 3.3+ or a compatible JRuby/TruffleRuby release
|
|
23
|
+
- CMDx 2.0+
|
|
19
24
|
|
|
20
25
|
## Installation
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
```sh
|
|
28
|
+
gem install cmdx-rspec
|
|
29
|
+
# - or -
|
|
30
|
+
bundle add cmdx-rspec --group test
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Require the library in `spec_helper.rb` (or equivalent):
|
|
23
34
|
|
|
24
35
|
```ruby
|
|
25
|
-
|
|
36
|
+
require "cmdx/rspec"
|
|
26
37
|
```
|
|
27
38
|
|
|
28
|
-
|
|
39
|
+
This loads every matcher under `RSpec::Matchers` and exposes the helpers under `CMDx::RSpec::Helpers`. See [Helpers](#helpers) for how to mix the helpers into your example groups.
|
|
40
|
+
|
|
41
|
+
### Relationship to the CMDx testing guide
|
|
29
42
|
|
|
30
|
-
|
|
43
|
+
The [CMDx testing docs](https://drexed.github.io/cmdx/testing/) use RSpec’s built-in predicate matchers on `CMDx::Result` — for example `be_success` calls `success?`, `be_failed` calls `failed?`, and `be_skipped` calls `skipped?`.
|
|
31
44
|
|
|
32
|
-
|
|
45
|
+
This gem adds matchers that pin both **state** and **status** via `result.to_h` (`be_successful`, `have_skipped`, `have_failed`), plus declaration matchers, chain helpers, and stub/build utilities. You can use both styles in the same suite.
|
|
33
46
|
|
|
34
|
-
|
|
47
|
+
For `execute!`, CMDx only raises when the outcome is **failed** (a `CMDx::Fault`, or the original exception when `result.cause` is a non-fault). Skips and successes return a result even in strict mode.
|
|
35
48
|
|
|
36
49
|
## Matchers
|
|
37
50
|
|
|
38
|
-
|
|
51
|
+
All result-oriented matchers raise `ArgumentError` when given a subject that isn't a `CMDx::Result`. Class-oriented matchers accept either the Task class or an instance.
|
|
39
52
|
|
|
40
|
-
|
|
53
|
+
### Result state & status
|
|
54
|
+
|
|
55
|
+
#### `be_successful`
|
|
56
|
+
|
|
57
|
+
Asserts a `CMDx::Result` completed with `state: "complete"` and `status: "success"`. Extra keyword args are forwarded to a `result.to_h` inclusion check, so any field can be constrained inline.
|
|
41
58
|
|
|
42
59
|
```ruby
|
|
43
|
-
|
|
44
|
-
|
|
60
|
+
expect(SomeTask.execute).to be_successful
|
|
61
|
+
expect(SomeTask.execute).to be_successful(metadata: { id: 1 })
|
|
62
|
+
```
|
|
45
63
|
|
|
46
|
-
|
|
47
|
-
|
|
64
|
+
#### `have_skipped`
|
|
65
|
+
|
|
66
|
+
Asserts a result was skipped (`state: "interrupted"`, `status: "skipped"`). Extra keyword args constrain other `result.to_h` fields.
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
expect(result).to have_skipped
|
|
70
|
+
expect(result).to have_skipped(reason: "out of stock", metadata: { code: "NA" })
|
|
48
71
|
```
|
|
49
72
|
|
|
50
|
-
|
|
73
|
+
#### `have_failed`
|
|
51
74
|
|
|
52
|
-
Asserts
|
|
75
|
+
Asserts a result failed (`state: "interrupted"`, `status: "failed"`). Extra keyword args constrain other `result.to_h` fields.
|
|
53
76
|
|
|
54
77
|
```ruby
|
|
55
|
-
|
|
56
|
-
|
|
78
|
+
expect(result).to have_failed
|
|
79
|
+
expect(result).to have_failed(
|
|
80
|
+
reason: "boom",
|
|
81
|
+
cause: be_a(NoMethodError)
|
|
82
|
+
)
|
|
83
|
+
```
|
|
57
84
|
|
|
58
|
-
|
|
59
|
-
expect(result).to have_skipped
|
|
85
|
+
#### `be_ok` / `be_ko`
|
|
60
86
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
)
|
|
67
|
-
end
|
|
87
|
+
`be_ok` delegates to `Result#ok?` (success or skipped). `be_ko` delegates to `Result#ko?` (skipped or failed). Skipped results satisfy **both** matchers.
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
expect(result).to be_ok
|
|
91
|
+
expect(result).to be_ko
|
|
68
92
|
```
|
|
69
93
|
|
|
70
|
-
|
|
94
|
+
#### `be_complete` / `be_interrupted`
|
|
71
95
|
|
|
72
|
-
|
|
96
|
+
State-only assertions. `be_complete` passes when `state == "complete"`; `be_interrupted` when `state == "interrupted"`.
|
|
73
97
|
|
|
74
98
|
```ruby
|
|
75
|
-
|
|
76
|
-
|
|
99
|
+
expect(result).to be_complete
|
|
100
|
+
expect(result).to be_interrupted
|
|
101
|
+
```
|
|
77
102
|
|
|
78
|
-
|
|
79
|
-
expect(result).to have_failed
|
|
103
|
+
### Result data
|
|
80
104
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
105
|
+
#### `have_empty_metadata` / `have_matching_metadata`
|
|
106
|
+
|
|
107
|
+
`have_empty_metadata` requires `metadata` to be empty. `have_matching_metadata` performs a partial-hash inclusion match (and delegates to `have_empty_metadata` when called with no args).
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
expect(result).to have_empty_metadata
|
|
111
|
+
expect(result).to have_matching_metadata(status_code: 500)
|
|
88
112
|
```
|
|
89
113
|
|
|
90
|
-
|
|
114
|
+
#### `have_empty_context` / `have_matching_context`
|
|
91
115
|
|
|
92
|
-
|
|
116
|
+
Same pattern for the result's context. Both accept a `Hash`, `CMDx::Context`, or `CMDx::Result` (the result's `.context` is unwrapped automatically).
|
|
93
117
|
|
|
94
118
|
```ruby
|
|
95
|
-
|
|
96
|
-
|
|
119
|
+
expect(result).to have_empty_context
|
|
120
|
+
expect(result).to have_matching_context(stored_id: 123)
|
|
121
|
+
```
|
|
97
122
|
|
|
98
|
-
|
|
99
|
-
|
|
123
|
+
### Errors
|
|
124
|
+
|
|
125
|
+
#### `have_no_errors`
|
|
126
|
+
|
|
127
|
+
Passes when the subject's `errors` collection is empty. Accepts a `CMDx::Result`, `CMDx::Task` instance, or `CMDx::Errors`.
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
expect(result).to have_no_errors
|
|
100
131
|
```
|
|
101
132
|
|
|
102
|
-
|
|
133
|
+
#### `have_errors_on`
|
|
103
134
|
|
|
104
|
-
Asserts
|
|
135
|
+
Asserts at least one error is present under `key`. Optional positional `messages` further constrain the matcher — all must be present.
|
|
105
136
|
|
|
106
137
|
```ruby
|
|
107
|
-
|
|
108
|
-
|
|
138
|
+
expect(result).to have_errors_on(:email)
|
|
139
|
+
expect(result).to have_errors_on(:email, "is required")
|
|
140
|
+
expect(task).to have_errors_on(:email, "is required", "is invalid")
|
|
141
|
+
```
|
|
109
142
|
|
|
110
|
-
|
|
111
|
-
|
|
143
|
+
### Execution metrics
|
|
144
|
+
|
|
145
|
+
#### `have_been_retried`
|
|
146
|
+
|
|
147
|
+
Passes when the result was retried at least once. Pass an integer to require an exact retry count.
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
expect(result).to have_been_retried
|
|
151
|
+
expect(result).to have_been_retried(3)
|
|
112
152
|
```
|
|
113
153
|
|
|
114
|
-
|
|
154
|
+
#### `have_been_rolled_back`
|
|
115
155
|
|
|
116
|
-
|
|
156
|
+
Passes when a failing task ran its rollback hook.
|
|
117
157
|
|
|
118
158
|
```ruby
|
|
119
|
-
|
|
120
|
-
|
|
159
|
+
expect(result).to have_been_rolled_back
|
|
160
|
+
```
|
|
121
161
|
|
|
122
|
-
|
|
123
|
-
|
|
162
|
+
#### `have_duration`
|
|
163
|
+
|
|
164
|
+
Asserts the result's duration (in milliseconds) falls within the supplied bounds. At least one of `:less_than` or `:greater_than` is required.
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
expect(result).to have_duration(less_than: 100)
|
|
168
|
+
expect(result).to have_duration(greater_than: 0.1, less_than: 50)
|
|
124
169
|
```
|
|
125
170
|
|
|
126
|
-
###
|
|
171
|
+
### Faults
|
|
172
|
+
|
|
173
|
+
#### `raise_cmdx_fault`
|
|
127
174
|
|
|
128
|
-
|
|
175
|
+
Block matcher that asserts a `CMDx::Fault` is raised. Optionally constrain by originating task class, reason, or underlying cause.
|
|
129
176
|
|
|
130
177
|
```ruby
|
|
131
|
-
|
|
132
|
-
|
|
178
|
+
expect { SomeTask.execute! }.to raise_cmdx_fault
|
|
179
|
+
expect { SomeTask.execute! }.to raise_cmdx_fault(SomeTask)
|
|
180
|
+
expect { SomeTask.execute! }
|
|
181
|
+
.to raise_cmdx_fault(SomeTask)
|
|
182
|
+
.with_reason(/invalid/)
|
|
183
|
+
.with_cause(MyError)
|
|
184
|
+
```
|
|
133
185
|
|
|
134
|
-
|
|
135
|
-
|
|
186
|
+
`with_reason` accepts a string (equality) or a Regexp; `with_cause` accepts a class (matches via `is_a?`) or a value (matched with `values_match?`).
|
|
187
|
+
|
|
188
|
+
In application code you can also use `CMDx::Fault.for?`, `Fault.reason?`, and `Fault.matches?` in `rescue` clauses; this matcher is the RSpec block equivalent for `execute!`.
|
|
189
|
+
|
|
190
|
+
### Chains
|
|
191
|
+
|
|
192
|
+
#### `have_chain_root`
|
|
193
|
+
|
|
194
|
+
Passes when the chain's root task class matches (or is a subclass of) `task_class`. Accepts a `CMDx::Chain` or `CMDx::Result`.
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
expect(result).to have_chain_root(MyWorkflow)
|
|
136
198
|
```
|
|
137
199
|
|
|
138
|
-
|
|
200
|
+
#### `have_chain_size`
|
|
139
201
|
|
|
140
|
-
|
|
202
|
+
Passes when the chain's size matches `expected`. Accepts a `CMDx::Chain` or `CMDx::Result`.
|
|
141
203
|
|
|
142
204
|
```ruby
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
205
|
+
expect(result).to have_chain_size(3)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
For failure propagation (`origin`, `caused_failure`, `threw_failure`), assert on the result directly — for example `expect(result.caused_failure.task).to eq(ValidateCard)` — as described in the [Result chain analysis](https://drexed.github.io/cmdx/outcomes/result/) docs.
|
|
209
|
+
|
|
210
|
+
### Task class declarations
|
|
211
|
+
|
|
212
|
+
These matchers introspect a Task class's configuration. They accept either the class or an instance.
|
|
213
|
+
|
|
214
|
+
#### `be_deprecated`
|
|
215
|
+
|
|
216
|
+
Asserts a Task class is marked deprecated. Optionally constrain the deprecation behavior via a positional value or a chained convenience method.
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
expect(SomeTask).to be_deprecated
|
|
220
|
+
expect(SomeTask).to be_deprecated.with_warning # :warn
|
|
221
|
+
expect(SomeTask).to be_deprecated.with_logging # :log
|
|
222
|
+
expect(SomeTask).to be_deprecated.with_error # :error
|
|
223
|
+
expect(SomeTask).to be_deprecated.with_behavior(:custom)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### `have_input` / `have_output`
|
|
227
|
+
|
|
228
|
+
Asserts the class declares the given input/output. Keyword args are matched (partial) against the parameter's serialized `to_h`.
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
expect(SomeTask).to have_input(:user_id)
|
|
232
|
+
expect(SomeTask).to have_input(:user_id, required: true)
|
|
233
|
+
expect(SomeTask).to have_input(:user_id, options: hash_including(coerce: :integer))
|
|
234
|
+
expect(SomeTask).to have_output(:total, required: true)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
#### `have_callback`
|
|
238
|
+
|
|
239
|
+
Asserts a callback is registered for `event`. Optional `callable` further constrains the match — by `==` for symbols/lambdas, or by `is_a?` when given a class.
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
expect(SomeTask).to have_callback(:before_execution)
|
|
243
|
+
expect(SomeTask).to have_callback(:before_execution, :authenticate!)
|
|
244
|
+
expect(SomeTask).to have_callback(:on_failed, AlertOnFailure)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
#### `have_middleware`
|
|
248
|
+
|
|
249
|
+
Asserts the class registered `middleware`. Class arguments match by `is_a?` or `==`; other values match by `==`.
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
expect(SomeTask).to have_middleware(LoggingMiddleware)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
#### `have_retry_on`
|
|
256
|
+
|
|
257
|
+
Asserts the class is configured to retry on `exception`. Keyword args check `CMDx::Retry` configuration values (`:limit`, `:delay`, `:max_delay`, `:jitter`).
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
expect(SomeTask).to have_retry_on(Net::OpenTimeout)
|
|
261
|
+
expect(SomeTask).to have_retry_on(Net::OpenTimeout, limit: 5, jitter: :exponential)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
#### `have_tag`
|
|
265
|
+
|
|
266
|
+
Asserts the subject carries `tag`. Accepts a Task class (reads `settings.tags`) or a `CMDx::Result` (reads `result.tags`).
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
expect(SomeTask).to have_tag(:critical)
|
|
270
|
+
expect(result).to have_tag(:critical)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Workflows
|
|
274
|
+
|
|
275
|
+
#### `have_pipeline_tasks`
|
|
276
|
+
|
|
277
|
+
Asserts a `CMDx::Workflow` class declares the given pipeline tasks. Order-sensitive by default; chain `.in_any_order` for set comparison.
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
expect(MyWorkflow).to have_pipeline_tasks(StepA, StepB, StepC)
|
|
281
|
+
expect(MyWorkflow).to have_pipeline_tasks(StepA, StepC, StepB).in_any_order
|
|
146
282
|
```
|
|
147
283
|
|
|
148
284
|
## Helpers
|
|
149
285
|
|
|
150
|
-
### Including
|
|
286
|
+
### Including helper modules
|
|
151
287
|
|
|
152
|
-
|
|
288
|
+
Mix into all example groups via RSpec config, or include in specific groups:
|
|
153
289
|
|
|
154
290
|
```ruby
|
|
155
291
|
RSpec.configure do |config|
|
|
@@ -157,162 +293,194 @@ RSpec.configure do |config|
|
|
|
157
293
|
end
|
|
158
294
|
```
|
|
159
295
|
|
|
160
|
-
Or include them in specific example groups:
|
|
161
|
-
|
|
162
296
|
```ruby
|
|
163
297
|
describe MyFeature do
|
|
164
298
|
include CMDx::RSpec::Helpers
|
|
165
|
-
|
|
166
|
-
# Your specs...
|
|
299
|
+
# ...
|
|
167
300
|
end
|
|
168
301
|
```
|
|
169
302
|
|
|
170
|
-
###
|
|
171
|
-
|
|
172
|
-
Helper methods for stubbing CMDx command execution.
|
|
303
|
+
### Result builders
|
|
173
304
|
|
|
174
|
-
|
|
305
|
+
Build a real `CMDx::Result` without stubbing `execute` — useful when a collaborator must return a result, when seeding a chain, or anywhere you need a result object in isolation. Extra keyword args (besides the documented ones) are forwarded to `task.new` as context data, same as the stubs below.
|
|
175
306
|
|
|
176
307
|
```ruby
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
stub_task_success(SomeTask)
|
|
180
|
-
stub_task_skip(SomeTask)
|
|
181
|
-
stub_task_fail(SomeTask)
|
|
182
|
-
|
|
183
|
-
# eg: SomeTask.execute!
|
|
184
|
-
stub_task_success!(SomeTask)
|
|
185
|
-
stub_task_skip!(SomeTask)
|
|
186
|
-
stub_task_fail!(SomeTask)
|
|
308
|
+
result = build_successful_result(snapshot:)
|
|
309
|
+
allow(FetchSnapshot).to receive(:execute).and_return(result)
|
|
187
310
|
|
|
188
|
-
|
|
189
|
-
|
|
311
|
+
build_skipped_result(reason: "not applicable")
|
|
312
|
+
build_failed_result(reason: "boom", cause: StandardError.new("kaput"))
|
|
313
|
+
build_echoed_result(upstream_failed_result)
|
|
190
314
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
315
|
+
build_result(:success, snapshot:, metadata: { id: 1 })
|
|
316
|
+
build_result(:failed, reason: "nope", strict: true, task: SomeTask)
|
|
317
|
+
build_result(:echo, upstream_result: failed_result)
|
|
318
|
+
```
|
|
194
319
|
|
|
195
|
-
|
|
196
|
-
stub_task_skip!(SomeTask, some: "value")
|
|
320
|
+
Options:
|
|
197
321
|
|
|
198
|
-
|
|
199
|
-
|
|
322
|
+
```ruby
|
|
323
|
+
build_successful_result(task: SomeTask, chain: existing_chain, context: CMDx::Context.new(foo: 1))
|
|
324
|
+
build_result(:success, deprecated: true, retries: 2, rolled_back: true)
|
|
200
325
|
```
|
|
201
326
|
|
|
202
|
-
|
|
327
|
+
### Stubs
|
|
328
|
+
|
|
329
|
+
Each stub builds a frozen `CMDx::Result` carrying the requested signal and wires it into a fresh `CMDx::Chain`, so callers see realistic execution shape without invoking the task's `work`. Any extra keyword args (besides the documented ones) are forwarded to `command.new` as context overrides.
|
|
330
|
+
|
|
331
|
+
#### Result-type stubs
|
|
203
332
|
|
|
204
333
|
```ruby
|
|
205
|
-
|
|
206
|
-
|
|
334
|
+
# Non-bang variants stub `SomeTask.execute`
|
|
335
|
+
stub_task_success(SomeTask) # alias: stub_task
|
|
336
|
+
stub_task_skip(SomeTask)
|
|
337
|
+
stub_task_fail(SomeTask)
|
|
338
|
+
|
|
339
|
+
# Bang variants stub `SomeTask.execute!`
|
|
340
|
+
stub_task_success!(SomeTask) # alias: stub_task! — returns result
|
|
341
|
+
stub_task_skip!(SomeTask) # returns result (skips do not raise under execute!)
|
|
342
|
+
stub_task_fail!(SomeTask) # raises CMDx::Fault, or re-raises cause when set
|
|
343
|
+
|
|
344
|
+
# Stub a specific argument signature
|
|
345
|
+
stub_task_success(SomeTask, some: "value") # SomeTask.execute(some: "value")
|
|
346
|
+
stub_task_skip!(SomeTask, some: "value") # SomeTask.execute!(some: "value")
|
|
347
|
+
```
|
|
207
348
|
|
|
208
|
-
|
|
209
|
-
end
|
|
349
|
+
Common options:
|
|
210
350
|
|
|
211
|
-
|
|
212
|
-
|
|
351
|
+
```ruby
|
|
352
|
+
stub_task_success(SomeTask, metadata: { id: 1 })
|
|
353
|
+
stub_task_skip!(SomeTask, reason: "out of stock")
|
|
354
|
+
stub_task_fail!(SomeTask, cause: NoMethodError.new("boom"))
|
|
355
|
+
```
|
|
213
356
|
|
|
214
|
-
|
|
215
|
-
end
|
|
357
|
+
#### Specialized stubs
|
|
216
358
|
|
|
217
|
-
|
|
218
|
-
|
|
359
|
+
```ruby
|
|
360
|
+
# Models the rescued StandardError -> failed signal path that Runtime
|
|
361
|
+
# takes when `work` raises something other than a Fault.
|
|
362
|
+
stub_task_error(SomeTask, Net::OpenTimeout, "boom")
|
|
219
363
|
|
|
220
|
-
|
|
221
|
-
|
|
364
|
+
# Models the `throw!`-then-propagate path used by nested tasks/workflows.
|
|
365
|
+
# `upstream_result` must be a failed CMDx::Result.
|
|
366
|
+
stub_task_throw(SomeTask, upstream_result)
|
|
367
|
+
|
|
368
|
+
# Returns a successful Result flagged as `deprecated?` without triggering
|
|
369
|
+
# the real Deprecation action.
|
|
370
|
+
stub_task_deprecated(SomeTask)
|
|
222
371
|
```
|
|
223
372
|
|
|
224
|
-
####
|
|
373
|
+
#### Workflow stubs
|
|
225
374
|
|
|
226
|
-
|
|
375
|
+
`stub_workflow_tasks` yields each distinct Task class reachable from a Workflow's pipeline (first-seen order) so you can stub them in one place.
|
|
227
376
|
|
|
228
377
|
```ruby
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
end
|
|
378
|
+
stub_workflow_tasks(MyWorkflow) do |task|
|
|
379
|
+
case task
|
|
380
|
+
when TaskB
|
|
381
|
+
stub_task_fail(task)
|
|
382
|
+
when TaskC
|
|
383
|
+
stub_task_skip(task)
|
|
384
|
+
else
|
|
385
|
+
stub_task_success(task)
|
|
238
386
|
end
|
|
387
|
+
end
|
|
239
388
|
|
|
240
|
-
|
|
389
|
+
MyWorkflow.execute
|
|
390
|
+
```
|
|
241
391
|
|
|
242
|
-
|
|
243
|
-
|
|
392
|
+
#### Unstubbing
|
|
393
|
+
|
|
394
|
+
Restores the original implementation. When `context` is supplied, only that argument signature is unstubbed.
|
|
395
|
+
|
|
396
|
+
```ruby
|
|
397
|
+
unstub_task(SomeTask) # SomeTask.execute
|
|
398
|
+
unstub_task!(SomeTask) # SomeTask.execute!
|
|
399
|
+
unstub_task(SomeTask, some: "value") # SomeTask.execute(some: "value")
|
|
244
400
|
```
|
|
245
401
|
|
|
246
|
-
|
|
402
|
+
### Mocks
|
|
403
|
+
|
|
404
|
+
Message expectations on `execute` / `execute!`. When `context` is supplied, the expectation is constrained to that signature.
|
|
247
405
|
|
|
248
406
|
```ruby
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
407
|
+
expect_task_execution(SomeTask)
|
|
408
|
+
expect_task_execution!(SomeTask)
|
|
409
|
+
expect_task_execution(SomeTask, some: "value")
|
|
410
|
+
|
|
411
|
+
expect_no_task_execution(SomeTask)
|
|
412
|
+
expect_no_task_execution!(SomeTask)
|
|
413
|
+
expect_no_task_execution(SomeTask, some: "value")
|
|
414
|
+
|
|
415
|
+
# after the fact; requires a spy
|
|
416
|
+
expected_task_execution(SomeTask)
|
|
417
|
+
expected_task_execution!(SomeTask)
|
|
418
|
+
expected_task_execution(SomeTask, some: "value")
|
|
419
|
+
|
|
420
|
+
expected_no_task_execution(SomeTask)
|
|
421
|
+
expected_no_task_execution!(SomeTask)
|
|
422
|
+
expected_no_task_execution(SomeTask, some: "value")
|
|
423
|
+
```
|
|
252
424
|
|
|
253
|
-
|
|
254
|
-
unstub_task!(SomeTask)
|
|
425
|
+
### RuboCop: `RSpec/NoExpectationExample`
|
|
255
426
|
|
|
256
|
-
|
|
257
|
-
end
|
|
427
|
+
The [RSpec/NoExpectationExample](https://docs.rubocop.org/rubocop-rspec/latest/cops_rspec.html#rspecnoexpectationexample) cop flags examples that do not call an RSpec expectation (for example `expect`, `is_expected`, or `should`). That is intentional for examples whose only assertion is a message expectation registered by [`expect_task_execution`](#mocks), [`expect_no_task_execution`](#mocks), or the bang variants — those helpers call `expect(command).to receive(...)` internally, but the example body often contains no top-level `expect`.
|
|
258
428
|
|
|
259
|
-
|
|
260
|
-
# eg: SomeTask.execute(some: "value")
|
|
261
|
-
unstub_task(SomeTask, some: "value")
|
|
429
|
+
RuboCop already treats method names matching `^expect_` and `^assert_` as expectations via the cop’s default `AllowedPatterns`. Add `^expected_` if you use the post-action [`expected_*`](#mocks) helpers (`expected_task_execution`, `expected_no_task_execution`, and their `!` variants), which assert with `have_received` instead of `receive`.
|
|
262
430
|
|
|
263
|
-
|
|
264
|
-
unstub_task!(SomeTask, some: "value")
|
|
431
|
+
Merge the following into `.rubocop.yml` (adjust if your file already configures this cop):
|
|
265
432
|
|
|
266
|
-
|
|
267
|
-
|
|
433
|
+
```yaml
|
|
434
|
+
RSpec/NoExpectationExample:
|
|
435
|
+
AllowedPatterns:
|
|
436
|
+
- "^expect_"
|
|
437
|
+
- "^expected_"
|
|
438
|
+
- "^assert_"
|
|
268
439
|
```
|
|
269
440
|
|
|
270
|
-
|
|
441
|
+
Examples that only stub or exercise behavior with an explicit matcher — for example `stub_task_success` followed by `expect(result).to be_successful` — do not need an allowed pattern. Workflow setup with `stub_workflow_tasks` is the same when you assert on outcomes afterward.
|
|
442
|
+
|
|
443
|
+
If you define your own expectation helpers, either name them with an `expect_` / `assert_` prefix or register them under `RSpec/Language/Expectations` in `.rubocop.yml` as described in the cop documentation.
|
|
271
444
|
|
|
272
|
-
|
|
445
|
+
### Diagnostics
|
|
273
446
|
|
|
274
|
-
####
|
|
447
|
+
#### `capture_cmdx_logs`
|
|
448
|
+
|
|
449
|
+
Captures lines written to a temporary `CMDx.configuration.logger` for the duration of the block. The previous logger is restored on exit.
|
|
275
450
|
|
|
276
451
|
```ruby
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
expect_no_task_execution(SomeTask)
|
|
452
|
+
logs = capture_cmdx_logs { MyCommand.execute }
|
|
453
|
+
expect(logs.join).to include("status=success")
|
|
454
|
+
```
|
|
281
455
|
|
|
282
|
-
|
|
283
|
-
expect_task_execution!(BangCommand)
|
|
284
|
-
expect_no_task_execution!(SomeTask)
|
|
456
|
+
#### `subscribe_telemetry`
|
|
285
457
|
|
|
286
|
-
|
|
287
|
-
end
|
|
458
|
+
Subscribes to telemetry events on a Task's telemetry registry for the duration of the block and returns every emitted event in order. Tasks subclassing `command` also fire (the registry is shared by reference until `dup`). Defaults to all events in `CMDx::Telemetry::EVENTS`.
|
|
288
459
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
460
|
+
```ruby
|
|
461
|
+
events = subscribe_telemetry(MyCommand, :task_executed) { MyCommand.execute }
|
|
462
|
+
expect(events.map(&:name)).to eq([:task_executed])
|
|
463
|
+
```
|
|
293
464
|
|
|
294
|
-
|
|
295
|
-
expect_task_execution!(SomeTask, some: "value")
|
|
296
|
-
expect_no_task_execution!(SomeTask, some: "value")
|
|
465
|
+
#### `with_cmdx_chain`
|
|
297
466
|
|
|
298
|
-
|
|
299
|
-
|
|
467
|
+
Captures the `CMDx::Chain` produced by the first root execution of `command` within the block. Returns `nil` if `command` didn't run as a root.
|
|
468
|
+
|
|
469
|
+
```ruby
|
|
470
|
+
chain = with_cmdx_chain(MyWorkflow) { MyWorkflow.execute }
|
|
471
|
+
expect(chain.size).to be > 1
|
|
300
472
|
```
|
|
301
473
|
|
|
302
474
|
## Development
|
|
303
475
|
|
|
304
|
-
|
|
476
|
+
Run `bin/setup` to install dependencies, then `rake spec` to run the tests. Use `bin/console` for an interactive prompt.
|
|
305
477
|
|
|
306
|
-
|
|
478
|
+
Release flow: bump `lib/cmdx/rspec/version.rb`, then `bundle exec rake release` to tag, push, and publish to [rubygems.org](https://rubygems.org).
|
|
307
479
|
|
|
308
480
|
## Contributing
|
|
309
481
|
|
|
310
|
-
Bug reports and pull requests are welcome
|
|
482
|
+
Bug reports and pull requests are welcome at <https://github.com/drexed/cmdx-rspec>. Contributors are expected to follow the [code of conduct](https://github.com/drexed/cmdx-rspec/blob/master/CODE_OF_CONDUCT.md).
|
|
311
483
|
|
|
312
484
|
## License
|
|
313
485
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
## Code of Conduct
|
|
317
|
-
|
|
318
|
-
Everyone interacting in the Cmdx::Rspec project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/drexed/cmdx-rspec/blob/master/CODE_OF_CONDUCT.md).
|
|
486
|
+
Released under the [MIT License](https://opensource.org/licenses/MIT).
|