Testing Idempotency¶
TestIdempotencyStore provides call recording, seedable state, and configurable claim behavior for testing idempotent operations.
Setup¶
var store = new TestIdempotencyStore();
var runtime = TestAppRuntime.Create()
.WithIdempotencyStore(store);
Call Recording¶
await program.RunAsync(runtime);
await Assert.That(store.ClaimCalls).Contains("webhook-123");
await Assert.That(store.ReleaseCalls).HasCount().EqualTo(0);
await Assert.That(store.GetCachedResponseCalls).HasCount().EqualTo(0);
Recording Properties¶
| Property | Type | Description |
|---|---|---|
ClaimCalls |
IReadOnlyList<string> |
Keys passed to TryClaimAsync |
IsClaimedCalls |
IReadOnlyList<string> |
Keys passed to IsClaimedAsync |
ReleaseCalls |
IReadOnlyList<string> |
Keys passed to ReleaseAsync |
GetCachedResponseCalls |
IReadOnlyList<string> |
Keys passed to TryGetCachedResponseAsync |
StoreCachedResponseCalls |
IReadOnlyList<string> |
Keys passed to StoreCachedResponseAsync |
ClaimedKeys |
IReadOnlyCollection<string> |
Currently claimed keys |
Seeding State¶
Pre-claim keys to simulate duplicate requests:
store.SeedClaimed("webhook-123");
// Now TryClaimAsync("webhook-123") returns false
var claimed = await store.TryClaimAsync("webhook-123");
await Assert.That(claimed).IsFalse();
Seed cached responses:
store.SeedCachedResponse("order-abc", new CachedResponse(
StatusCode: 201,
Headers: new() { ["Content-Type"] = "application/json" },
Body: """{"id": "order-abc"}"""u8.ToArray()
));
Testing Duplicate Detection¶
[Test]
public async Task WebhookHandler_SkipsDuplicate()
{
var store = new TestIdempotencyStore();
store.SeedClaimed("webhook-already-processed");
var runtime = TestAppRuntime.Create()
.WithIdempotencyStore(store);
await WebhookHandler.Process("webhook-already-processed", payload).RunAsync(runtime);
// Handler should have checked but NOT processed
await Assert.That(store.ClaimCalls).Contains("webhook-already-processed");
// Verify no side effects occurred (e.g., no orders created)
}