You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Tidy testing SDK design — drop A/B/C exploration framing
The "Approach A vs B vs C" framing only made sense in the brainstorming
chat where the alternatives had been spelled out earlier. The committed
spec should present the chosen design directly and explain why the
service-client interface is right on its own merits.
Copy file name to clipboardExpand all lines: Libraries/src/Amazon.Lambda.DurableExecution/docs/design/testing-sdk-design.md
+7-11Lines changed: 7 additions & 11 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -54,9 +54,9 @@ The new testing package depends on:
54
54
-`Amazon.Lambda.TestUtilities` (project reference) — `TestLambdaContext`, `TestLambdaLogger` for the runner's `ILambdaContext` substitute.
55
55
-`Amazon.Lambda.Serialization.SystemTextJson` (package reference) — `DefaultLambdaJsonSerializer` is the fallback when `TestRunnerOptions.Serializer` is null.
56
56
57
-
### Interception strategy: `IDurableServiceClient` seam (Approach B)
The runtime SDK already isolates outbound durable RPCs behind a single class — `LambdaDurableServiceClient`, currently `internal sealed`. Both reference SDKs (Python, JavaScript) chose to inject a service-client interface for testing rather than fake the broader Lambda client; .NET follows the same convergent design.
59
+
The runtime SDK already isolates outbound durable RPCs behind a single class — `LambdaDurableServiceClient`, currently `internal sealed`. We promote that class to implement an `internal IDurableServiceClient` interface and inject a fake implementation from the testing package. The orchestration loop in `DurableFunction.WrapAsync` runs unmodified; only the two outbound RPCs (`CheckpointAsync`, `GetExecutionStateAsync`) are swapped. This keeps the testing-package surface tiny (two methods to fake) and exercises the **real** runtime engine — replay logic, checkpoint batching, termination handling, serializer dispatch — on every test.
60
60
61
61
Three changes to the runtime package — all `internal`, no public-API impact:
Because the seam is the service client, the orchestration loop drives the **real** runtime engine — every replay-consistency check, every operation-id allocation, every batch-flush boundary that ships in production code is exercised by every test.
137
137
138
-
### Why not a fake `IAmazonLambda` (Approach A)
138
+
### Why an interface and not a broader fake
139
139
140
-
`IAmazonLambda` exposes ~88 members; faking it requires ~80 stubs throwing `NotImplementedException` (or subclassing `AmazonLambdaClient` and overriding the 5 durable RPCs plus `InvokeAsync`). Both reference SDKs rejected this surface and converged on a service-client interface instead. The decoupling from AWSSDK request/response shapes pays off when AWSSDK adds a new durable RPC (the interface is a contract we own; the SDK shape is not).
141
-
142
-
### Why not a standalone orchestrator (Approach C)
143
-
144
-
Java reimplements the orchestration loop in its testing package. The cost: ~2,500 lines of test-runner code that has to track every behavioral change in the runtime. .NET avoids this by injecting at the service-client boundary and reusing the production engine.
140
+
`IDurableServiceClient` exposes only the two methods the runtime needs to talk to the durable execution service. A test fake implements those two methods; everything else stays in the production engine. This is the same shape both reference SDKs (Python's `DurableServiceClient`, JavaScript's `CheckpointApiClient`) settled on. The decoupling from AWSSDK request/response shapes pays off when AWSSDK adds a new durable RPC: the interface is a contract we own, and the runtime keeps mapping AWSSDK shapes to our own `Operation` / `OperationUpdate` types in one place (`LambdaDurableServiceClient`), unchanged.
145
141
146
142
---
147
143
@@ -484,7 +480,7 @@ Name matching is exact-string, with ARN parsing to extract `:function:NAME[:qual
484
480
485
481
### What is *not* reimplemented
486
482
487
-
`ExecutionState`, `TerminationManager`, `CheckpointBatcher`, `OperationIdGenerator`, the `*Operation` classes, `LambdaSerializerHelper.GetRequired`, every replay-consistency check — all from the runtime package, exercised as-is. That is the value of Approach B.
483
+
`ExecutionState`, `TerminationManager`, `CheckpointBatcher`, `OperationIdGenerator`, the `*Operation` classes, `LambdaSerializerHelper.GetRequired`, every replay-consistency check — all from the runtime package, exercised as-is. That is the value of injecting at the service-client boundary instead of reimplementing the orchestrator.
488
484
489
485
---
490
486
@@ -980,7 +976,7 @@ Coverage:
980
976
-`InvokeAsync` to a registered plain (non-durable) sibling completes.
981
977
- Replay-consistency violations surface `NonDeterministicExecutionException` exactly as production does.
982
978
983
-
This is the most important layer — it proves Approach B works end-to-end.
979
+
This is the most important layer — it proves the `IDurableServiceClient` injection covers the full runtime surface end-to-end.
984
980
985
981
### Layer 3 — snapshot tests of generated handler shape
Per the parent design doc: **~1.5 weeks** for full Local + Cloud + RegisterFunction + step inspection. This design doesn't change that estimate — Approach B's reuse of the production engine keeps the testing-package code small (~800–1200 lines, comparable to Python's ~3000 because Python reimplements more checkpoint-validation logic).
1087
+
Per the parent design doc: **~1.5 weeks** for full Local + Cloud + RegisterFunction + step inspection. This design doesn't change that estimate — reusing the production engine via the `IDurableServiceClient` seam keeps the testing-package code small (~800–1200 lines).
0 commit comments