Skip to content

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

var runtime = TestAppRuntime.Create()
    .WithCacheStore(new InMemoryCacheStore());

InMemoryCacheStore is backed by ConcurrentDictionary — no Redis required for tests.

File Storage

var runtime = TestAppRuntime.Create()
    .WithFileStore(new InMemoryFileStore());

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

Notifications

var channel = new InMemoryNotificationChannel();
var runtime = TestAppRuntime.Create()
    .WithNotificationChannel(channel);

await program.RunAsync(runtime);

await Assert.That(channel.SentNotifications).HasCount().EqualTo(1);