@temporal-architect/claude-plugin 0.9.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.
- package/LICENSE +21 -0
- package/README.md +38 -0
- package/package.json +37 -0
- package/skills/MANIFEST.json +373 -0
- package/skills/MANIFEST.md +121 -0
- package/skills/temporal-architect/SKILL.md +99 -0
- package/skills/temporal-architect/reference/decomposition.md +78 -0
- package/skills/temporal-architect-author-go/README.md +16 -0
- package/skills/temporal-architect-author-go/SKILL.md +191 -0
- package/skills/temporal-architect-author-go/SUBAGENT_ADOPTION.md +161 -0
- package/skills/temporal-architect-author-go/reference/activity-call.md +73 -0
- package/skills/temporal-architect-author-go/reference/activity-def.md +54 -0
- package/skills/temporal-architect-author-go/reference/assignment.md +36 -0
- package/skills/temporal-architect-author-go/reference/await-all.md +104 -0
- package/skills/temporal-architect-author-go/reference/await-one.md +193 -0
- package/skills/temporal-architect-author-go/reference/await-timer.md +35 -0
- package/skills/temporal-architect-author-go/reference/close.md +71 -0
- package/skills/temporal-architect-author-go/reference/composite-patterns.md +176 -0
- package/skills/temporal-architect-author-go/reference/condition.md +56 -0
- package/skills/temporal-architect-author-go/reference/control-flow.md +151 -0
- package/skills/temporal-architect-author-go/reference/dependency-resolution.md +29 -0
- package/skills/temporal-architect-author-go/reference/detach.md +52 -0
- package/skills/temporal-architect-author-go/reference/heartbeat.md +84 -0
- package/skills/temporal-architect-author-go/reference/nexus-service-def.md +73 -0
- package/skills/temporal-architect-author-go/reference/nexus.md +35 -0
- package/skills/temporal-architect-author-go/reference/options.md +138 -0
- package/skills/temporal-architect-author-go/reference/promise.md +73 -0
- package/skills/temporal-architect-author-go/reference/proto-driven.md +197 -0
- package/skills/temporal-architect-author-go/reference/query-handler.md +34 -0
- package/skills/temporal-architect-author-go/reference/signal-handler.md +73 -0
- package/skills/temporal-architect-author-go/reference/three-layer-testing.md +173 -0
- package/skills/temporal-architect-author-go/reference/types.md +72 -0
- package/skills/temporal-architect-author-go/reference/update-handler.md +64 -0
- package/skills/temporal-architect-author-go/reference/worker.md +215 -0
- package/skills/temporal-architect-author-go/reference/workflow-call.md +37 -0
- package/skills/temporal-architect-author-go/reference/workflow-def.md +45 -0
- package/skills/temporal-architect-author-infra/README.md +16 -0
- package/skills/temporal-architect-author-infra/SKILL.md +132 -0
- package/skills/temporal-architect-author-infra/reference/tcld.md +112 -0
- package/skills/temporal-architect-author-infra/reference/terraform.md +125 -0
- package/skills/temporal-architect-design/README.md +16 -0
- package/skills/temporal-architect-design/SKILL.md +224 -0
- package/skills/temporal-architect-design/reference/LANGUAGE.md +5 -0
- package/skills/temporal-architect-design/reference/anti-patterns.md +332 -0
- package/skills/temporal-architect-design/reference/common-errors.md +88 -0
- package/skills/temporal-architect-design/reference/core-principles.md +52 -0
- package/skills/temporal-architect-design/reference/design-checklist.md +59 -0
- package/skills/temporal-architect-design/reference/namespaces.md +84 -0
- package/skills/temporal-architect-design/reference/notation-examples.md +304 -0
- package/skills/temporal-architect-design/reference/notation-reference.md +70 -0
- package/skills/temporal-architect-design/reference/primitives-reference.md +65 -0
- package/skills/temporal-architect-design/reference/project-discovery-subagent.md +80 -0
- package/skills/temporal-architect-design/reference/reverse-engineering.md +53 -0
- package/skills/temporal-architect-design/reference/twf-conventions.md +43 -0
- package/skills/temporal-architect-design/reference/workflow-boundaries.md +43 -0
- package/skills/temporal-architect-design/topics/activities-advanced.md +358 -0
- package/skills/temporal-architect-design/topics/activities-advanced.twf +107 -0
- package/skills/temporal-architect-design/topics/child-workflows.md +347 -0
- package/skills/temporal-architect-design/topics/child-workflows.twf +171 -0
- package/skills/temporal-architect-design/topics/long-running.md +230 -0
- package/skills/temporal-architect-design/topics/long-running.twf +100 -0
- package/skills/temporal-architect-design/topics/nexus.md +248 -0
- package/skills/temporal-architect-design/topics/nexus.twf +148 -0
- package/skills/temporal-architect-design/topics/patterns.md +469 -0
- package/skills/temporal-architect-design/topics/patterns.twf +346 -0
- package/skills/temporal-architect-design/topics/promises-conditions.md +179 -0
- package/skills/temporal-architect-design/topics/promises-conditions.twf +213 -0
- package/skills/temporal-architect-design/topics/signals-queries-updates.md +319 -0
- package/skills/temporal-architect-design/topics/signals-queries-updates.twf +234 -0
- package/skills/temporal-architect-design/topics/task-queues.md +205 -0
- package/skills/temporal-architect-design/topics/task-queues.twf +184 -0
- package/skills/temporal-architect-design/topics/testing.md +437 -0
- package/skills/temporal-architect-design/topics/testing.twf +177 -0
- package/skills/temporal-architect-design/topics/timers-scheduling.md +131 -0
- package/skills/temporal-architect-design/topics/timers-scheduling.twf +129 -0
- package/skills/temporal-architect-design/topics/versioning.md +434 -0
- package/skills/temporal-architect-design/topics/versioning.twf +174 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# Testing Temporal Workflows
|
|
2
|
+
|
|
3
|
+
> **Example:** [`testing.twf`](./testing.twf)
|
|
4
|
+
|
|
5
|
+
Strategies for testing workflows, activities, and ensuring determinism.
|
|
6
|
+
|
|
7
|
+
## Testing Pyramid for Temporal
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
┌─────────────────┐
|
|
11
|
+
│ End-to-End │ Few, slow, high confidence
|
|
12
|
+
│ Tests │
|
|
13
|
+
├─────────────────┤
|
|
14
|
+
│ Integration │ Test with real services
|
|
15
|
+
│ Tests │
|
|
16
|
+
├─────────────────┤
|
|
17
|
+
│ Workflow │ Mock activities
|
|
18
|
+
│ Unit Tests │
|
|
19
|
+
├─────────────────┤
|
|
20
|
+
│ Activity │ Mock clients/SDKs
|
|
21
|
+
│ Unit Tests │
|
|
22
|
+
└─────────────────┘ Many, fast, isolated
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Activity Unit Testing
|
|
28
|
+
|
|
29
|
+
Test activities in isolation by mocking external dependencies.
|
|
30
|
+
|
|
31
|
+
### Pattern
|
|
32
|
+
|
|
33
|
+
> Note: Activity body implementations and test assertions are SDK-level code, not TWF notation.
|
|
34
|
+
|
|
35
|
+
```pseudo
|
|
36
|
+
# Activity implementation
|
|
37
|
+
activity ProcessPayment(payment: Payment) -> PaymentResult:
|
|
38
|
+
# Validate
|
|
39
|
+
if payment.amount <= 0:
|
|
40
|
+
raise InvalidPaymentError("Amount must be positive")
|
|
41
|
+
|
|
42
|
+
# Call external service
|
|
43
|
+
result = paymentClient.charge(payment)
|
|
44
|
+
|
|
45
|
+
return PaymentResult{
|
|
46
|
+
transactionId: result.id,
|
|
47
|
+
status: result.status
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Test
|
|
51
|
+
test "ProcessPayment succeeds with valid payment":
|
|
52
|
+
mockPaymentClient = Mock()
|
|
53
|
+
mockPaymentClient.charge.returns({id: "txn-123", status: "success"})
|
|
54
|
+
|
|
55
|
+
result = ProcessPayment(Payment{amount: 100, cardId: "card-1"})
|
|
56
|
+
|
|
57
|
+
assert result.transactionId == "txn-123"
|
|
58
|
+
assert result.status == "success"
|
|
59
|
+
assert mockPaymentClient.charge.called_with(Payment{amount: 100})
|
|
60
|
+
|
|
61
|
+
test "ProcessPayment fails with invalid amount":
|
|
62
|
+
expect_error InvalidPaymentError:
|
|
63
|
+
ProcessPayment(Payment{amount: -50})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### What to Test in Activities
|
|
67
|
+
- Input validation
|
|
68
|
+
- Error handling for external failures
|
|
69
|
+
- Correct client/SDK usage
|
|
70
|
+
- Return value construction
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Workflow Unit Testing
|
|
75
|
+
|
|
76
|
+
Test workflow logic by mocking activities. Use Temporal's test framework.
|
|
77
|
+
|
|
78
|
+
### Pattern
|
|
79
|
+
|
|
80
|
+
```twf
|
|
81
|
+
workflow RenewalProcessing(renewal: Renewal) -> (RenewalResult):
|
|
82
|
+
activity ValidateRenewalRequest(renewal) -> validated
|
|
83
|
+
if not validated.success:
|
|
84
|
+
close fail(RenewalResult{status: "invalid"})
|
|
85
|
+
|
|
86
|
+
activity ChargeRenewalPayment(renewal.payment) -> payment
|
|
87
|
+
activity ActivateRenewedPlan(renewal)
|
|
88
|
+
|
|
89
|
+
close complete(RenewalResult{status: "completed", paymentId: payment.id})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```pseudo
|
|
93
|
+
# Test
|
|
94
|
+
test "RenewalProcessing completes successfully":
|
|
95
|
+
env = TestWorkflowEnvironment()
|
|
96
|
+
|
|
97
|
+
# Mock activities
|
|
98
|
+
env.mock_activity(ValidateRenewalRequest, returns: {success: true})
|
|
99
|
+
env.mock_activity(ChargeRenewalPayment, returns: {id: "pay-123"})
|
|
100
|
+
env.mock_activity(ActivateRenewedPlan, returns: {})
|
|
101
|
+
|
|
102
|
+
# Execute workflow
|
|
103
|
+
result = env.execute_workflow(RenewalProcessing, Renewal{id: "renew-1"})
|
|
104
|
+
|
|
105
|
+
# Assert result
|
|
106
|
+
assert result.status == "completed"
|
|
107
|
+
assert result.paymentId == "pay-123"
|
|
108
|
+
|
|
109
|
+
# Assert activity calls
|
|
110
|
+
assert env.activity_called(ValidateRenewalRequest, with: Renewal{id: "renew-1"})
|
|
111
|
+
assert env.activity_called(ChargeRenewalPayment)
|
|
112
|
+
assert env.activity_called(ActivateRenewedPlan)
|
|
113
|
+
|
|
114
|
+
test "RenewalProcessing returns invalid for failed validation":
|
|
115
|
+
env = TestWorkflowEnvironment()
|
|
116
|
+
|
|
117
|
+
env.mock_activity(ValidateRenewalRequest, returns: {success: false, error: "bad renewal"})
|
|
118
|
+
|
|
119
|
+
result = env.execute_workflow(RenewalProcessing, Renewal{id: "renew-1"})
|
|
120
|
+
|
|
121
|
+
assert result.status == "invalid"
|
|
122
|
+
assert not env.activity_called(ChargeRenewalPayment) # Not called
|
|
123
|
+
assert not env.activity_called(ActivateRenewedPlan) # Not called
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### What to Test in Workflows
|
|
127
|
+
- Activity call ordering
|
|
128
|
+
- Conditional logic (correct branch taken)
|
|
129
|
+
- Error handling (activity failures)
|
|
130
|
+
- Signal/query handlers
|
|
131
|
+
- Timeout behavior
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Replay Testing (Determinism Verification)
|
|
136
|
+
|
|
137
|
+
Verify workflows are deterministic by replaying against recorded history.
|
|
138
|
+
|
|
139
|
+
### Why Replay Testing
|
|
140
|
+
|
|
141
|
+
```text
|
|
142
|
+
Version 1: Workflow runs, generates history
|
|
143
|
+
Version 2: Workflow code changes
|
|
144
|
+
Replay Test: Run version 2 against version 1 history
|
|
145
|
+
Result: PASS (deterministic) or FAIL (non-determinism detected)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Pattern
|
|
149
|
+
|
|
150
|
+
```pseudo
|
|
151
|
+
# Record workflow history
|
|
152
|
+
test "record RenewalProcessing history":
|
|
153
|
+
env = TestWorkflowEnvironment()
|
|
154
|
+
# ... execute workflow ...
|
|
155
|
+
history = env.get_workflow_history()
|
|
156
|
+
save_to_file("renewal_processing_v1.history", history)
|
|
157
|
+
|
|
158
|
+
# Replay test
|
|
159
|
+
test "RenewalProcessing replays deterministically":
|
|
160
|
+
history = load_from_file("renewal_processing_v1.history")
|
|
161
|
+
|
|
162
|
+
env = TestWorkflowEnvironment()
|
|
163
|
+
result = env.replay_workflow(RenewalProcessing, history)
|
|
164
|
+
|
|
165
|
+
assert result.replay_successful
|
|
166
|
+
assert not result.non_determinism_errors
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Maintaining History Files
|
|
170
|
+
- Store history files in version control
|
|
171
|
+
- Update when workflow signature changes
|
|
172
|
+
- Keep multiple versions for migration testing
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Testing Signals and Queries
|
|
177
|
+
|
|
178
|
+
### Signal Testing
|
|
179
|
+
|
|
180
|
+
```twf
|
|
181
|
+
workflow LeaveRequestApproval(request: LeaveRequest) -> (Decision):
|
|
182
|
+
await one:
|
|
183
|
+
signal Approved:
|
|
184
|
+
close complete(Decision{status: "approved"})
|
|
185
|
+
signal Rejected:
|
|
186
|
+
close complete(Decision{status: "rejected"})
|
|
187
|
+
timer(1h):
|
|
188
|
+
close complete(Decision{status: "timeout"})
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
```pseudo
|
|
192
|
+
# Test
|
|
193
|
+
test "LeaveRequestApproval handles Approved signal":
|
|
194
|
+
env = TestWorkflowEnvironment()
|
|
195
|
+
|
|
196
|
+
# Start workflow
|
|
197
|
+
handle = env.start_workflow(LeaveRequestApproval, LeaveRequest{id: "req-1"})
|
|
198
|
+
|
|
199
|
+
# Send signal
|
|
200
|
+
env.signal_workflow(handle, Approved, {approver: "alice"})
|
|
201
|
+
|
|
202
|
+
# Get result
|
|
203
|
+
result = env.get_workflow_result(handle)
|
|
204
|
+
assert result.status == "approved"
|
|
205
|
+
|
|
206
|
+
test "LeaveRequestApproval handles timeout":
|
|
207
|
+
env = TestWorkflowEnvironment()
|
|
208
|
+
|
|
209
|
+
handle = env.start_workflow(LeaveRequestApproval, LeaveRequest{id: "req-1"})
|
|
210
|
+
|
|
211
|
+
# Skip time forward
|
|
212
|
+
env.skip_time(2h)
|
|
213
|
+
|
|
214
|
+
result = env.get_workflow_result(handle)
|
|
215
|
+
assert result.status == "timeout" # or however timeout is handled
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Query Testing
|
|
219
|
+
|
|
220
|
+
```twf
|
|
221
|
+
workflow StatusWorkflow(orderId: string) -> (OrderResult):
|
|
222
|
+
query GetStatus() -> (string):
|
|
223
|
+
return status
|
|
224
|
+
|
|
225
|
+
status = "pending"
|
|
226
|
+
activity StartProcessing(orderId)
|
|
227
|
+
|
|
228
|
+
status = "processing"
|
|
229
|
+
activity ProcessItems(orderId)
|
|
230
|
+
|
|
231
|
+
status = "completed"
|
|
232
|
+
close complete(OrderResult{status: status, orderId: orderId})
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
```pseudo
|
|
236
|
+
# Test
|
|
237
|
+
test "GetStatus query returns current status":
|
|
238
|
+
env = TestWorkflowEnvironment()
|
|
239
|
+
|
|
240
|
+
# Mock activity to block
|
|
241
|
+
blocker = env.mock_activity(ProcessItems, blocks: true)
|
|
242
|
+
|
|
243
|
+
handle = env.start_workflow(StatusWorkflow, "order-1")
|
|
244
|
+
|
|
245
|
+
# Query while processing
|
|
246
|
+
status = env.query_workflow(handle, GetStatus)
|
|
247
|
+
assert status == "processing"
|
|
248
|
+
|
|
249
|
+
# Unblock activity
|
|
250
|
+
blocker.unblock({})
|
|
251
|
+
|
|
252
|
+
# Query after completion
|
|
253
|
+
env.wait_for_workflow(handle)
|
|
254
|
+
status = env.query_workflow(handle, GetStatus)
|
|
255
|
+
assert status == "completed"
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Testing Timers
|
|
261
|
+
|
|
262
|
+
Use time-skipping to test timer behavior without waiting.
|
|
263
|
+
|
|
264
|
+
```twf
|
|
265
|
+
workflow ReminderWorkflow(userId: string):
|
|
266
|
+
activity SendFirstReminder(userId)
|
|
267
|
+
|
|
268
|
+
await timer(24h)
|
|
269
|
+
|
|
270
|
+
activity SendSecondReminder(userId)
|
|
271
|
+
|
|
272
|
+
await timer(48h)
|
|
273
|
+
|
|
274
|
+
activity SendFinalReminder(userId)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
```pseudo
|
|
278
|
+
# Test
|
|
279
|
+
test "ReminderWorkflow sends reminders at correct intervals":
|
|
280
|
+
env = TestWorkflowEnvironment()
|
|
281
|
+
|
|
282
|
+
env.mock_activity(SendFirstReminder, returns: {})
|
|
283
|
+
env.mock_activity(SendSecondReminder, returns: {})
|
|
284
|
+
env.mock_activity(SendFinalReminder, returns: {})
|
|
285
|
+
|
|
286
|
+
handle = env.start_workflow(ReminderWorkflow, "user-1")
|
|
287
|
+
|
|
288
|
+
# First reminder sent immediately
|
|
289
|
+
assert env.activity_called(SendFirstReminder)
|
|
290
|
+
assert not env.activity_called(SendSecondReminder)
|
|
291
|
+
|
|
292
|
+
# Skip 24 hours
|
|
293
|
+
env.skip_time(24h)
|
|
294
|
+
|
|
295
|
+
assert env.activity_called(SendSecondReminder)
|
|
296
|
+
assert not env.activity_called(SendFinalReminder)
|
|
297
|
+
|
|
298
|
+
# Skip 48 more hours
|
|
299
|
+
env.skip_time(48h)
|
|
300
|
+
|
|
301
|
+
assert env.activity_called(SendFinalReminder)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Testing Child Workflows
|
|
307
|
+
|
|
308
|
+
```twf
|
|
309
|
+
workflow ParentWorkflow(data: Data) -> (Result):
|
|
310
|
+
workflow ChildWorkflow(data.item) -> childResult
|
|
311
|
+
close complete(Result{childData: childResult})
|
|
312
|
+
|
|
313
|
+
workflow ChildWorkflow(item: Item) -> (ChildResult):
|
|
314
|
+
activity ProcessItem(item)
|
|
315
|
+
close complete(ChildResult{processed: true})
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
```pseudo
|
|
319
|
+
# Test parent in isolation
|
|
320
|
+
test "ParentWorkflow calls child correctly":
|
|
321
|
+
env = TestWorkflowEnvironment()
|
|
322
|
+
|
|
323
|
+
env.mock_child_workflow(ChildWorkflow, returns: {processed: true})
|
|
324
|
+
|
|
325
|
+
result = env.execute_workflow(ParentWorkflow, Data{item: Item{id: "1"}})
|
|
326
|
+
|
|
327
|
+
assert result.childData.processed == true
|
|
328
|
+
assert env.child_workflow_called(ChildWorkflow, with: Item{id: "1"})
|
|
329
|
+
|
|
330
|
+
# Test parent and child together
|
|
331
|
+
test "ParentWorkflow integration with ChildWorkflow":
|
|
332
|
+
env = TestWorkflowEnvironment()
|
|
333
|
+
|
|
334
|
+
# Mock only activities, let child workflow run
|
|
335
|
+
env.mock_activity(ProcessItem, returns: {})
|
|
336
|
+
|
|
337
|
+
result = env.execute_workflow(ParentWorkflow, Data{item: Item{id: "1"}})
|
|
338
|
+
|
|
339
|
+
assert result.childData.processed == true
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## Integration Testing
|
|
345
|
+
|
|
346
|
+
Test with real Temporal server (local or test cluster).
|
|
347
|
+
|
|
348
|
+
### Setup
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
# Start local Temporal for testing
|
|
352
|
+
temporal server start-dev
|
|
353
|
+
|
|
354
|
+
# Or use testcontainers
|
|
355
|
+
test_environment = TemporalTestContainer()
|
|
356
|
+
test_environment.start()
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Pattern
|
|
360
|
+
|
|
361
|
+
```pseudo
|
|
362
|
+
test "RenewalProcessing end-to-end":
|
|
363
|
+
# Use real Temporal client
|
|
364
|
+
client = TemporalClient(address: "localhost:7233")
|
|
365
|
+
|
|
366
|
+
# Start real worker
|
|
367
|
+
worker = Worker(
|
|
368
|
+
client: client,
|
|
369
|
+
task_queue: "test-queue",
|
|
370
|
+
workflows: [RenewalProcessing],
|
|
371
|
+
activities: [ValidateRenewalRequest, ChargeRenewalPayment, ActivateRenewedPlan]
|
|
372
|
+
)
|
|
373
|
+
worker.start_async()
|
|
374
|
+
|
|
375
|
+
# Execute workflow
|
|
376
|
+
handle = client.start_workflow(
|
|
377
|
+
RenewalProcessing,
|
|
378
|
+
Renewal{id: "test-renew-1"},
|
|
379
|
+
workflow_id: "test-renew-1"
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# Wait for result
|
|
383
|
+
result = handle.result(timeout: 30s)
|
|
384
|
+
|
|
385
|
+
assert result.status == "completed"
|
|
386
|
+
|
|
387
|
+
# Cleanup
|
|
388
|
+
worker.stop()
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## Testing Best Practices
|
|
394
|
+
|
|
395
|
+
### Do's
|
|
396
|
+
|
|
397
|
+
| Practice | Rationale |
|
|
398
|
+
|----------|-----------|
|
|
399
|
+
| Mock activities in workflow tests | Isolate workflow logic |
|
|
400
|
+
| Use replay tests | Catch non-determinism early |
|
|
401
|
+
| Test failure paths | Verify error handling |
|
|
402
|
+
| Use time-skipping | Fast timer tests |
|
|
403
|
+
| Test signal ordering | Validate async behavior |
|
|
404
|
+
|
|
405
|
+
### Don'ts
|
|
406
|
+
|
|
407
|
+
| Anti-Pattern | Problem |
|
|
408
|
+
|--------------|---------|
|
|
409
|
+
| Testing deterministic logic via integration tests | Slow, flaky |
|
|
410
|
+
| Skipping replay tests | Non-determinism in production |
|
|
411
|
+
| Mocking workflow internals | Brittle tests |
|
|
412
|
+
| Real time waits | Slow tests |
|
|
413
|
+
| Testing Temporal internals | Not your responsibility |
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## Test Coverage Checklist
|
|
418
|
+
|
|
419
|
+
### Activity
|
|
420
|
+
- [ ] Valid inputs produce correct output
|
|
421
|
+
- [ ] Invalid inputs produce appropriate errors
|
|
422
|
+
- [ ] External service failures handled
|
|
423
|
+
- [ ] Retryable vs non-retryable errors classified
|
|
424
|
+
|
|
425
|
+
### Workflow
|
|
426
|
+
- [ ] Happy path completes successfully
|
|
427
|
+
- [ ] Each conditional branch tested
|
|
428
|
+
- [ ] Activity failures handled correctly
|
|
429
|
+
- [ ] Signals processed correctly
|
|
430
|
+
- [ ] Queries return correct state
|
|
431
|
+
- [ ] Timeouts handled appropriately
|
|
432
|
+
- [ ] Replay test passes
|
|
433
|
+
|
|
434
|
+
### Integration
|
|
435
|
+
- [ ] End-to-end happy path
|
|
436
|
+
- [ ] Cross-service communication
|
|
437
|
+
- [ ] Failure recovery
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Source: testing.md
|
|
2
|
+
# Patterns: simple testable workflow/activity pairs
|
|
3
|
+
# Domain: subscription renewals + leave-of-absence approvals + reminder cadences
|
|
4
|
+
# + status-tracked status flow + condition-driven subscription manager.
|
|
5
|
+
# Each workflow is intentionally small and isolated so unit tests can drive
|
|
6
|
+
# signals/updates and assert on activity calls in isolation.
|
|
7
|
+
|
|
8
|
+
# --- Basic linear workflow: renewal processing ---
|
|
9
|
+
|
|
10
|
+
workflow RenewalProcessing(renewal: Renewal) -> (RenewalResult):
|
|
11
|
+
activity ValidateRenewalRequest(renewal) -> validated
|
|
12
|
+
if (validated.success == false):
|
|
13
|
+
close fail(RenewalResult{status: "invalid"})
|
|
14
|
+
|
|
15
|
+
activity ChargeRenewalPayment(renewal.payment) -> payment
|
|
16
|
+
activity ActivateRenewedPlan(renewal)
|
|
17
|
+
|
|
18
|
+
close complete(RenewalResult{status: "completed", paymentId: payment.id})
|
|
19
|
+
|
|
20
|
+
# --- Workflow with timer: scheduled reminder cadence ---
|
|
21
|
+
|
|
22
|
+
workflow ReminderWorkflow(userId: string):
|
|
23
|
+
activity SendFirstReminder(userId)
|
|
24
|
+
|
|
25
|
+
await timer(24h)
|
|
26
|
+
|
|
27
|
+
activity SendSecondReminder(userId)
|
|
28
|
+
|
|
29
|
+
await timer(48h)
|
|
30
|
+
|
|
31
|
+
activity SendFinalReminder(userId)
|
|
32
|
+
close complete
|
|
33
|
+
|
|
34
|
+
# --- Workflow with signals: leave-of-absence approval ---
|
|
35
|
+
|
|
36
|
+
workflow LeaveRequestApproval(request: LeaveRequest) -> (Decision):
|
|
37
|
+
signal Approved(approver: string):
|
|
38
|
+
approved = true
|
|
39
|
+
approverName = approver
|
|
40
|
+
|
|
41
|
+
signal Rejected(approver: string, reason: string):
|
|
42
|
+
rejected = true
|
|
43
|
+
rejectReason = reason
|
|
44
|
+
|
|
45
|
+
approved = false
|
|
46
|
+
rejected = false
|
|
47
|
+
approverName = ""
|
|
48
|
+
rejectReason = ""
|
|
49
|
+
|
|
50
|
+
activity NotifyManagers(request)
|
|
51
|
+
|
|
52
|
+
await one:
|
|
53
|
+
signal Approved:
|
|
54
|
+
close complete(Decision{status: "approved", approver: approverName})
|
|
55
|
+
signal Rejected:
|
|
56
|
+
close fail(Decision{status: "rejected", reason: rejectReason})
|
|
57
|
+
timer(1h):
|
|
58
|
+
close complete(Decision{status: "timeout"})
|
|
59
|
+
|
|
60
|
+
# --- Workflow with query: status flow ---
|
|
61
|
+
|
|
62
|
+
workflow StatusWorkflow(orderId: string) -> (OrderResult):
|
|
63
|
+
query GetStatus() -> (string):
|
|
64
|
+
return status
|
|
65
|
+
|
|
66
|
+
status = "pending"
|
|
67
|
+
activity StartProcessing(orderId)
|
|
68
|
+
|
|
69
|
+
status = "processing"
|
|
70
|
+
activity ProcessItems(orderId)
|
|
71
|
+
|
|
72
|
+
status = "completed"
|
|
73
|
+
close complete(OrderResult{status: status, orderId: orderId})
|
|
74
|
+
|
|
75
|
+
# --- Complex workflow for testing continue-as-new, conditions, and updates ---
|
|
76
|
+
|
|
77
|
+
workflow SubscriptionManager(userId: string, sub: Subscription):
|
|
78
|
+
state:
|
|
79
|
+
condition renewed
|
|
80
|
+
|
|
81
|
+
update RenewSubscription(payment: Payment) -> (RenewResult):
|
|
82
|
+
activity ValidatePayment(payment) -> validation
|
|
83
|
+
if (validation.ok):
|
|
84
|
+
set renewed
|
|
85
|
+
return RenewResult{accepted: true}
|
|
86
|
+
else:
|
|
87
|
+
return RenewResult{accepted: false, reason: validation.error}
|
|
88
|
+
|
|
89
|
+
query GetSubscription() -> (Subscription):
|
|
90
|
+
return sub
|
|
91
|
+
|
|
92
|
+
for:
|
|
93
|
+
# Wait for renewal or expiration
|
|
94
|
+
await one:
|
|
95
|
+
renewed:
|
|
96
|
+
activity ExtendSubscription(userId, sub) -> sub
|
|
97
|
+
unset renewed
|
|
98
|
+
timer(30d):
|
|
99
|
+
activity SendExpirationWarning(userId)
|
|
100
|
+
await one:
|
|
101
|
+
renewed:
|
|
102
|
+
activity ExtendSubscription(userId, sub) -> sub
|
|
103
|
+
unset renewed
|
|
104
|
+
timer(7d):
|
|
105
|
+
activity CancelSubscription(userId)
|
|
106
|
+
close complete
|
|
107
|
+
|
|
108
|
+
# Periodic state snapshot for continue-as-new
|
|
109
|
+
close continue_as_new(userId, sub)
|
|
110
|
+
|
|
111
|
+
# --- Supporting activities ---
|
|
112
|
+
|
|
113
|
+
activity ValidateRenewalRequest(renewal: Renewal) -> (ValidationResult):
|
|
114
|
+
return validate(renewal)
|
|
115
|
+
|
|
116
|
+
activity ChargeRenewalPayment(payment: Payment) -> (PaymentResult):
|
|
117
|
+
return charge(payment)
|
|
118
|
+
|
|
119
|
+
activity ActivateRenewedPlan(renewal: Renewal):
|
|
120
|
+
activate(renewal)
|
|
121
|
+
|
|
122
|
+
activity SendFirstReminder(userId: string):
|
|
123
|
+
remind(userId, "first")
|
|
124
|
+
|
|
125
|
+
activity SendSecondReminder(userId: string):
|
|
126
|
+
remind(userId, "second")
|
|
127
|
+
|
|
128
|
+
activity SendFinalReminder(userId: string):
|
|
129
|
+
remind(userId, "final")
|
|
130
|
+
|
|
131
|
+
activity NotifyManagers(request: LeaveRequest):
|
|
132
|
+
notify(request.managers)
|
|
133
|
+
|
|
134
|
+
activity StartProcessing(orderId: string):
|
|
135
|
+
start(orderId)
|
|
136
|
+
|
|
137
|
+
activity ProcessItems(orderId: string):
|
|
138
|
+
process(orderId)
|
|
139
|
+
|
|
140
|
+
activity ValidatePayment(payment: Payment) -> (PaymentValidation):
|
|
141
|
+
return validate(payment)
|
|
142
|
+
|
|
143
|
+
activity ExtendSubscription(userId: string, sub: Subscription) -> (Subscription):
|
|
144
|
+
return extend(userId, sub)
|
|
145
|
+
|
|
146
|
+
activity SendExpirationWarning(userId: string):
|
|
147
|
+
warn(userId)
|
|
148
|
+
|
|
149
|
+
activity CancelSubscription(userId: string):
|
|
150
|
+
cancel(userId)
|
|
151
|
+
|
|
152
|
+
# --- Worker and namespace ---
|
|
153
|
+
|
|
154
|
+
worker testingWorker:
|
|
155
|
+
workflow RenewalProcessing
|
|
156
|
+
workflow ReminderWorkflow
|
|
157
|
+
workflow LeaveRequestApproval
|
|
158
|
+
workflow StatusWorkflow
|
|
159
|
+
workflow SubscriptionManager
|
|
160
|
+
activity ValidateRenewalRequest
|
|
161
|
+
activity ChargeRenewalPayment
|
|
162
|
+
activity ActivateRenewedPlan
|
|
163
|
+
activity SendFirstReminder
|
|
164
|
+
activity SendSecondReminder
|
|
165
|
+
activity SendFinalReminder
|
|
166
|
+
activity NotifyManagers
|
|
167
|
+
activity StartProcessing
|
|
168
|
+
activity ProcessItems
|
|
169
|
+
activity ValidatePayment
|
|
170
|
+
activity ExtendSubscription
|
|
171
|
+
activity SendExpirationWarning
|
|
172
|
+
activity CancelSubscription
|
|
173
|
+
|
|
174
|
+
namespace testing:
|
|
175
|
+
worker testingWorker
|
|
176
|
+
options:
|
|
177
|
+
task_queue: "testing"
|