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,_RecordsSaveCall→Create()(spy/contract tests)_CreatesLoan,_SetsDueDate,_Fails_WhenNotFound→CreateConfigured()(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.