Skip to content

Testing Multiple Runtimes

Two runtimes means two test runtimes. Each context is tested independently — Catalog tests don't need Lending to exist, and vice versa.

Per-Context Isolation

[TestRuntime<CatalogRuntime>]
public partial class TestCatalogRuntime;

[TestRuntime<LendingRuntime>]
public partial class TestLendingRuntime;

Each generates its own Create() and CreateConfigured(). Each wires its own stores and event queues. They don't interfere with each other.

Two Ways to Test

Create() CreateConfigured()
Returns TestRuntime Production runtime
Stores Test* (spy + seed) InMemory* (or real)
Events EventQueueContext (assertable) Channels initialized, not assertable
Hook None OnConfigure / OnConfigureIntegration
Use for Verifying contracts (events, saves) Verifying behavior (outcomes)

Unit Tests with Spies

[Test]
public async Task BorrowBook_PublishesDomainEvent()
{
    var runtime = TestLendingRuntime.Create();
    runtime.PatronStore.Seed(new Patron { Id = patronId, Name = "Alice", Email = "alice@example.com" });

    await LendingDispatch.BorrowBook(patronId, bookId).RunAsync(runtime);

    runtime.LendingDomainEvents.AssertContains<DomainEvent.BookBorrowed>();
}

Create() gives you: - .Seed() — pre-populate stores without going through commands - .SaveCalls — verify what was saved - .AssertContains<T>() — verify events were enqueued

Behavior Tests

[Test]
public async Task BorrowBook_CreatesLoan()
{
    var runtime = TestLendingRuntime.CreateConfigured();

    var fin = await (
        from _ in LendingStore.Patrons.Save<LendingRuntime>(patron)
        from loanId in LendingDispatch.BorrowBook(patronId, BookId.New())
        from loans in LendingDispatch.GetPatronLoans(patronId)
        select loans
    ).RunAsync(runtime);

    await Assert.That(fin).IsSuccMatching(r => r.Data.Count == 1);
}

CreateConfigured() returns the production runtime. Seed data via dispatch. Verify outcomes via queries. This test works identically against InMemory stores and real Postgres — flip with one environment variable:

dotnet test                       # InMemory stores
INTEGRATION=true dotnet test      # real Postgres (via OnConfigureIntegration hook)

Naming Convention

Both types live in the same test file. The method name tells you which path it uses:

  • _PublishesDomainEvent, _RecordsSaveCallCreate() (spy/contract tests)
  • _CreatesLoan, _SetsDueDate, _Fails_WhenNotFoundCreateConfigured() (behavior tests)

Integration Testing with Real Infrastructure

Implement OnConfigureIntegration to swap in real stores:

[TestRuntime<LendingRuntime>]
public partial class TestLendingRuntime
{
    partial void OnConfigureIntegration() =>
        IntegrationDefaults.Configure(this);
}

IntegrationDefaults is a static helper that starts Testcontainers, runs migrations, and provides real store implementations from DI. See Integration Testing for the full pattern.

Cross-Context Integration Tests

To test the full event flow (Lending publishes → Catalog subscribes), compose both runtimes in a single test host. This is an advanced pattern covered in the integration testing docs — the key idea is that both runtimes share the same in-process event channel, so events flow between them synchronously.