Skip to content

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)
}

Reset

store.Reset();
// All calls cleared, all claimed keys removed, all cached responses cleared