Testing
This page is how tests are written so a signature change edits one file rather than a hundred call sites. AgentUp.Architecture.Tests already enforces the rules.
AgentUp.Verification.Tests was written to this standard deliberately and is the worked
example to copy. The rules below are enforced by AgentUp.Architecture.Tests, so this page
describes what the build already checks rather than a convention to remember.
Test data goes through builders
Production DTOs and models are not constructed in tests. Each test project keeps a builder
per coupling hotspot in its Support/ folder:
var workspace = ServerDomain.Workspace()
.Named("Shop")
.WithApplication(new ApplicationDefinitionBuilder("Api", "dotnet run").WithPort(8080))
.Build();
A builder starts from a sensible default and lets a test state only the attribute it is
actually about. RegisterWorkspaceRequest has five positional fields and five collection
properties; adding one now changes RegisterWorkspaceRequestBuilder rather than the hundred
registrations across the suite.
TestDataBuilders enforces this in two halves:
- A production DTO or model under
Features/*/DTOsorFeatures/*/Models, with more than three constructor parameters, constructed more than fifteen times in one test project, must have aSupport/<Type>Builder.csin that project. A narrower type reads fine positionally and is left alone; the threshold catches a type as it becomes a hotspot. - Once that builder exists, nothing else in the project constructs the type, including
through an implicit
new(...), which is judged by the type it is written into.
Domain vocabulary is shared and named
Each test project has one file naming its world: ServerDomain, CliDomain,
DesktopDomain, DebugDomain, VerificationDomain. One canonical workspace, one
application, one port, one queued commit, named once and reused.
Assert.That(overview.WorktreePath, Is.EqualTo(ServerDomain.WorktreePath));
Tests refer to those names rather than inventing path, branch and commit strings, so a reader can tell at a glance whether two tests are talking about the same workspace or different ones. Where a test needs one value to be specific, it injects that value through a builder instead of restating the whole world.
Variation is a call, not a new fixture method
Canned fixtures fail at the first test that needs "the same thing, but different". A builder returned from the domain gives that test a seam:
DesktopDomain.Workspace().Stopped().Build();
DesktopDomain.WorkspaceServing(port: 10200).Build();
Domain helpers that stand for a shape - WorkspaceServing, WorkspaceWithApplications -
return a builder rather than a finished object, so the next variation does not need another
method on a shared file.
Setup lives in the test
New fixtures arrange their subject inside the test, through a local helper the test calls with explicit parameters:
private static async Task<(AppDriver App, ItemsControl Transcript)> OpenTranscriptWithAThoughtAsync()
A [SetUp] method moves the arrangement out of the test that depends on it, and every test
in the fixture then pays for whatever the slowest one needed. TestFixtureSetup bans
[SetUp] and [OneTimeSetUp] in new fixtures, over a ratchet baseline of the fixtures that
predate the rule: entries come out as the setup moves, and the rule fails on an entry that is
already clean, so the file cannot drift into fiction. [TearDown] is untouched - releasing a
temp directory says nothing about what a test can be read to mean.
Composition helpers compose services, not data
AgentUp.Server.Tests/Fake/ServerTestComposition.cs assembles the object graphs that are too
large to build at every test. It composes services only; test data comes from the builders.
Every parameter that changes behaviour under test - the configuration provider, the identity
provider, the platform branch - is required rather than defaulted behind a ??, so reading a
call still tells you what the subject was given. Where a plain composition is common it gets
a named overload that states what it stands for, rather than a null-coalescing default that
hides the choice.
One behaviour per test
TestAssertionDensity caps a test method at ten assertions. Roughly three quarters of the
suite already carries one to three, which is healthy; the cap exists to catch the tail. A
method asserting twenty or thirty things names one behaviour and covers several, so a failure
does not say what broke and the method can only grow.
Splitting usually means either [TestCase] per instance of the same rule:
[TestCase("DISPLAY", ":123")]
[TestCase("GDK_BACKEND", "x11")]
public async Task Prepare_putsTheHostedDisplayIntoTheApplicationEnvironment(string name, string expected)
or one test per concern over a shared local helper. Assume.That counts towards the cap. Neither
Assume.That nor Assert.Ignore may decide at runtime that platform, privilege, or live system
state makes a test inapplicable: that reports success without having run. Cover those branches
through an injected capability provider instead. The runtime-skip baseline is empty. It rejects
additions and stale entries, so a new Assert.Ignore or Assume.That on live platform state
fails the suite.
Category=LoopInference is the Server loop kernel driven by the locally-inferring
test-agent-infer ACP agent. It is slow and ciOnly. Keep it out of
run-dotnet-tests.sh, the fast server check, and the test-kind watchdog.
Concurrency and restart for autonomous workspaces run in the normal E2E
tier. Category=LoopSoak is hundreds of scripted tasks plus bounded-growth
asserts. It is slow and ciOnly. Keep it out of those same suites.
loop-scripted is the fast AgentUp.TestAgents.Tests Scripted slice plus
Server ScriptedLoop Provider tests.
Mobile npm test discovers src/features/**/*.test.ts recursively, so a new feature's tests run without editing the script. MobileFeatureTestCoverage fails a src/features/<slice> that has production TypeScript outside components/ with no tests, and a providers/ folder whose production files have no sibling tests. React context controllers and Expo hooks stay thin and delegate to those providers. Component rendering is not gated: do not add react-native-testing-library for product screens. WebAudit npm test runs in the Mobile CI jobs on every pull request, not only the release publish step.
Rules assert properties, not membership
An architecture rule that pins an exact set fails on any legitimate addition while verifying
no behaviour. ReleasePayloadLayout used to assert the payload directories were exactly
cli, desktop, installer, server, tray; it now asserts what CI depends on - that every
payload directory is non-empty and unique - which a new payload satisfies and a colliding one
does not.