Testing
Repl.Testing provides an in-memory harness that runs your full command graph without a real terminal — typed assertions, multi-session, interaction recording.
dotnet add package Repl.Testingusing Repl.Testing;
// Reuse your app factoryvar host = ReplTestHost.Create(app =>{ app.Map("list", (IContactStore store) => store.All()); app.Map("add {name} {email:email}", (string name, string email, IContactStore store) => { store.Add(name, email); return Results.Success($"Added {name}."); });}, services =>{ services.AddSingleton<IContactStore, ContactStore>();});Single-command execution
Section titled “Single-command execution”RunCommandAsync returns a CommandExecution with the output text, exit code, and the raw result object:
[TestMethod][Description("Verifies the full route-to-store binding: add command exits 0, and the new entry is visible in a subsequent list.")]public async Task When_AddContact_Then_SuccessAndPersisted(){ await using var session = await host.OpenSessionAsync();
var result = await session.RunCommandAsync("add Alice alice@example.com");
result.ExitCode.Should().Be(0); result.OutputText.Should().Contain("Alice");
var list = await session.RunCommandAsync("list --json"); var contacts = list.ReadJson<IEnumerable<Contact>>(); contacts.Should().ContainSingle(c => c.Name == "Alice");}To access the typed handler return value directly, use GetResult<T>():
var execution = await session.RunCommandAsync("contacts 1 show");var contact = execution.GetResult<Contact>();contact.Name.Should().Be("Alice");Multi-step sequences
Section titled “Multi-step sequences”Scoped routes work as full paths — no special navigation method needed:
[TestMethod][Description("Commands in a scoped context run via the full route path.")]public async Task When_ScopedCommand_Then_ExecutesCorrectly(){ await using var session = await host.OpenSessionAsync();
await session.RunCommandAsync("add Alice alice@example.com");
var result = await session.RunCommandAsync("client 1 show"); result.ExitCode.Should().Be(0); result.OutputText.Should().Contain("Alice");}Multi-session tests
Section titled “Multi-session tests”[TestMethod][Description("Changes made by one session are visible to another via shared store.")]public async Task When_TwoSessions_Then_SharedStateIsVisible(){ await using var session1 = await host.OpenSessionAsync(); await using var session2 = await host.OpenSessionAsync();
await session1.RunCommandAsync("add Bob bob@example.com");
var result = await session2.RunCommandAsync("list --json"); var contacts = result.ReadJson<IEnumerable<Contact>>(); contacts.Should().ContainSingle(c => c.Name == "Bob");}Testing interactions (prompts)
Section titled “Testing interactions (prompts)”Pass prompt answers as a dictionary keyed by prompt name. They are injected as --answer:name=value flags:
[TestMethod][Description("Interactive add command reads name and email from supplied answers.")]public async Task When_InteractiveAdd_WithAnswers_Then_ContactCreated(){ await using var session = await host.OpenSessionAsync();
await session.RunCommandAsync("add", new Dictionary<string, string> { ["name"] = "Bob", ["email"] = "bob@example.com", });
var result = await session.RunCommandAsync("list --json"); var contacts = result.ReadJson<IEnumerable<Contact>>(); contacts.Should().ContainSingle(c => c.Name == "Bob");}For session-wide answers (applied to every command), set them on SessionDescriptor.Answers when opening the session:
await using var session = await host.OpenSessionAsync(new SessionDescriptor{ Answers = new Dictionary<string, string> { ["confirm"] = "yes" }});Interaction timeline
Section titled “Interaction timeline”Inspect the interaction events recorded during execution:
var result = await session.RunCommandAsync("add", answers);result.InteractionEvents.Should().HaveCount(2);TimelineEvents combines output, interactions, and the final result into a chronological sequence.
Further reading
Section titled “Further reading”- Best Practices — test-first approach, testing at the command level, and interaction supply patterns.
- Pipelining & Lifecycle — exit codes and how
Results.*maps to observable outcomes.