Module Test Patterns¶
Each Deepstaging module generates test-friendly infrastructure. This page covers the testing patterns specific to each module.
DataStore¶
[DataStore] generates InMemory{Entity}Store implementations backed by ConcurrentDictionary, and Test{Entity}Store doubles with call recording.
// Use InMemory store for stateful tests
var runtime = TestAppRuntime.Create()
.WithArticleStore(new InMemoryArticleStore());
// Or use the generated Test store for call recording
var runtime = TestAppRuntime.Create()
.WithStubArticleStore(stub => stub);
Call Recording¶
Test{Entity}Store records all calls:
var store = new TestArticleStore();
var runtime = TestAppRuntime.Create()
.WithArticleStore(store);
await program.RunAsync(runtime);
await Assert.That(store.SaveCalls).HasCount().EqualTo(1);
await Assert.That(store.DeleteCalls).HasCount().EqualTo(0);
Seeding¶
Seed data without triggering call recording:
var store = new InMemoryArticleStore();
store.Seed(new Article(ArticleId.New(), "Test", "Body"));
var runtime = TestAppRuntime.Create()
.WithArticleStore(store);
HttpClient¶
[HttpClient] generates Test{TypeName} with configurable responses and call recording.
var runtime = TestAppRuntime.Create()
.WithStubUsersClient(stub => stub
.OnGetUser(userId => Task.FromResult(new User("alice"))));
Call Recording Shapes¶
Recording shape depends on parameter count:
| Parameters | Recording Type |
|---|---|
| 0 | int CallCount |
| 1 | List<T> Calls |
| 2+ | List<(T1, T2, ...)> Calls |
EventQueue¶
Test event queues without starting the background worker:
var channel = new EventQueueChannel<OrderEvent>();
var runtime = TestAppRuntime.Create()
.WithOrderChannel(channel);
await program.RunAsync(runtime);
var success = channel.TryRead(out var evt);
await Assert.That(success).IsTrue();
await Assert.That(evt).IsTypeOf<OrderPlaced>();
// Or drain all events
var events = channel.DrainAll();
await Assert.That(events).HasCount().EqualTo(3);
EventQueueContext — Per-Test Isolation¶
For parallel test safety, use EventQueueContext<TEvent> instead of a shared channel. It creates an isolated channel per test using AsyncLocal, so parallel tests don't interfere:
// Auto-wired by TestRuntime:
var runtime = TestFundraisingRuntime.CreateConfigured();
await program.RunAsync(runtime);
// Assert on events with type-safe helpers:
var donated = runtime.FundraisingIntegrationEvents.AssertSingle<DonationReceived>();
await Assert.That(donated.Amount).IsEqualTo(25m);
runtime.FundraisingIntegrationEvents.AssertSingle<RecurringDonationCreated>();
runtime.FundraisingIntegrationEvents.AssertCount(2);
Assertion API¶
| Method | Description |
|---|---|
Events |
All events in enqueue order (reusable across assertions) |
OfType<T>() |
Filter events by type |
AssertNone() |
No events were enqueued |
AssertNone<T>() |
No events of type T |
AssertSingle() |
Exactly one event (any type) |
AssertSingle<T>() |
Exactly one event of type T — returns it |
AssertContains<T>() |
At least one event of type T — returns first |
AssertCount(n) |
Total event count matches |
AssertCount<T>(n) |
Count of type T matches |
Clear() |
Reset recorded events |
The context uses ListEventTransport<TEvent> internally — a list-backed transport that captures events in memory without a background worker.
Background Jobs¶
Test that jobs are scheduled without running them:
var scheduler = new InMemoryJobScheduler();
var runtime = TestAppRuntime.Create()
.WithJobScheduler(scheduler);
await program.RunAsync(runtime);
await Assert.That(scheduler.ScheduledJobs).HasCount().EqualTo(1);
Cache¶
InMemoryCacheStore is backed by ConcurrentDictionary — no Redis required for tests.
File Storage¶
Audit Trail¶
var store = new InMemoryAuditStore();
var runtime = TestAppRuntime.Create()
.WithAuditStore(store);
await program.RunAsync(runtime);
var entries = await store.GetByEntityAsync("Article", articleId.ToString());
await Assert.That(entries).HasCount().EqualTo(1);