One host, one lifetime
The run builds the host once from Setup.cs. This lesson opens it: what the base class does before the first test and after the last, and the line the builder refuses to cross.
- Say when the run's host is built, started and stopped.
- Read the run layer of a trace as the host's own record.
- Tell what Configure may still change and what Build() has closed off.
- Capabilities and the host (lesson 1).
- The sample cloned. Reading the archives alone also works.
The scenario
The sample composes everything in one method, Setup.Configure, and no test calls it. The runner calls it once per assembly, before any test runs, and stops the host once after the last one. Both ends belong to the base class, not to the suite.
That single lifetime is why a capability, a container or a report sink is a run decision. It is also why the builder has a last step: after Build(), a registration would mutate a live run instead of composing one, so the builder refuses it.
The class the runner calls
samples/Northstar.ProtoTest/Setup.cs is short at the top because the base class does most of the work:
1[SetUpFixture]2public sealed class Setup : ProtoTestAssembly3{4protected override void Configure(IProtoHostBuilder builder)5{6var configuration = LoadConfiguration();7var run = NorthstarRun.From(configuration);8run.PrepareOwnedStore();910ConfigureInfrastructure(builder, run);11ConfigureApplications(builder, run);12ConfigureDomain(builder, run);13// ... tracing, sinks, the run gate and messaging14}15}
- NUnit's once-per-assembly hook
The base class carries the attribute, so deriving from ProtoTestAssembly is the whole registration. The other runners have their own assembly hook.
- One class, one run
Everything the run shares is written here, once.
- Called before the first test
Configure receives the builder; a test body can never add to it. That is what makes the composition readable as one list.
samples/Northstar.ProtoTest/Setup.cs, trimmed to the declaration and the three composition calls.The base class is the same idea for every runner. This is NUnit's:
1[SetUpFixture]2public abstract class ProtoTestAssembly3: ProtoTestAssemblyHost<ProtoTestAssembly>, IProtoTestAssemblyHost<ProtoTestAssembly>4{5[OneTimeSetUp]6public Task GlobalSetUp() => StartAsync(Configure);78[OneTimeTearDown]9public Task GlobalTearDown() => StopAsync();1011protected abstract void Configure(IProtoHostBuilder builder);12}
- Start once
Before any test, the base builds the host from Configure and starts it.
- Stop once
After the last test, the base stops the host and disposes it. Both calls throw if the lifetime already ran, so a second start cannot reopen the run.
src/ProtoTest.NUnit/ProtoTestAssembly.cs. The xUnit, xUnit v3, TUnit and MSTest packages ship the same base under their own assembly hooks.Configure, then Build, then start
The base class hides three steps that every runner takes in the same order:
1var builder = new ProtoHostBuilder();2configure(builder);3var host = builder.Build();4await host.StartAsync().ConfigureAwait(false);
- Configure composes
Your Setup.Configure adds applications, integrations, sinks, hooks and gates to one builder.
- Build is the terminal step
It validates the composition and creates the host. After it, any registration throws: "The ProtoHostBuilder has already built a ProtoHost; configure a new builder instead."
- Start opens the run
Run hooks run, capabilities are recorded, infrastructure starts, the trace listener attaches. Only then can the first test start.
src/ProtoTest.Core/ProtoTestHostLifetime.cs, the shared start path behind every adapter.The terminal rule blocks a second registration. It would change a run that already started. A second Build() throws the same message, and a second start throws ProtoHost has already been initialized for this assembly. The rule applies to every entry on the builder, from ConfigureServices to AddRunGate.
The run's two ends in the trace
The host records its own work in the trace's run layer, outside every test. l1-first-journey.prototrace holds both ends. The two timestamps are the ones the archive records, and both are UTC: the run's own start and the test's setup entry.
| Record | Reading |
|---|---|
Seven capability entities: Playwright, Data, Sheets, GraphQL, REST, ASP.NET Core, SQL | recorded when the host started, before the test's setup opened |
Run start 18:37:08.072Z, the test's setup opens 18:37:09.060Z | the host was alive about 1.0 second before the test |
Release · messaging:broker, 0.3 ms | the host releases a run piece after the test |
Release · readiness:application:Northstar web, 0.0 ms | the readiness probe is a run resource |
Release · application:loopback:Northstar web, 5.0 ms | the listener the browser journey follows |
The report's Resources section lists the run pieces as Registered | the report is written before the releases, so its snapshot says so |
A run with no tests still has that shape. l2-broker-skip.prototrace holds only the three releases and no test resource at all, because the broker journey skipped before its lifecycle started. The host existed, served the capability list, and was stopped exactly once.
The state entities in the same archive carry their own stamps with a local offset, so the viewer prints two clocks in one run. Compare the two timestamps above with each other, and read a duration rather than a wall time when you compare an entity with a span.
One host, not one per test
ProtoTestAssembly.Host is the static host a [ProtoTest] test resolves, and its lifetime belongs to the closed generic type, so two adapter assemblies loaded in one process each own their host instead of sharing one. When the run ends, stopping clears it: a later lookup reports that the host was not initialized and repeats the runner's own hint, such as Ensure your setup class inherits from ProtoTestAssembly. The next run in the same process starts a fresh host.
That is the whole contract: compose in one place, build once, start once, stop once. Everything else in this level is a decision about what goes into that one host.
Checkpoint
The first journey's archive lists seven capabilities and three run resources. Who releases the three, and when does that happen relative to the test?
Release · messaging:broker, Release · readiness:application:Northstar web and Release · application:loopback:Northstar web, after the test's teardown. The test never owned them, so no test teardown could have released them.What you learned
- One host per test assembly: built from Configure once, started before the first test, stopped after the last.
- Build() is terminal; every registration after it throws instead of changing a live run.
- The run layer of a trace records the host's own work: capabilities, resources, gates and releases.