Skip to main content

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.

Level 2, lesson 2About 8 minutes
By the end
  • 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.
Before you start
  • 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:

Setup.cs3 notes
1[SetUpFixture]
2public sealed class Setup : ProtoTestAssembly
3{
4protected override void Configure(IProtoHostBuilder builder)
5{
6var configuration = LoadConfiguration();
7var run = NorthstarRun.From(configuration);
8run.PrepareOwnedStore();
9
10ConfigureInfrastructure(builder, run);
11ConfigureApplications(builder, run);
12ConfigureDomain(builder, run);
13// ... tracing, sinks, the run gate and messaging
14}
15}
  1. 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.

  2. One class, one run

    Everything the run shares is written here, once.

  3. 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.

From 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:

ProtoTestAssembly.cs2 notes
1[SetUpFixture]
2public abstract class ProtoTestAssembly
3: ProtoTestAssemblyHost<ProtoTestAssembly>, IProtoTestAssemblyHost<ProtoTestAssembly>
4{
5[OneTimeSetUp]
6public Task GlobalSetUp() => StartAsync(Configure);
7
8[OneTimeTearDown]
9public Task GlobalTearDown() => StopAsync();
10
11protected abstract void Configure(IProtoHostBuilder builder);
12}
  1. Start once

    Before any test, the base builds the host from Configure and starts it.

  2. 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.

From 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:

ProtoTestHostLifetime.cs3 notes
1var builder = new ProtoHostBuilder();
2configure(builder);
3var host = builder.Build();
4await host.StartAsync().ConfigureAwait(false);
  1. Configure composes

    Your Setup.Configure adds applications, integrations, sinks, hooks and gates to one builder.

  2. 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."

  3. Start opens the run

    Run hooks run, capabilities are recorded, infrastructure starts, the trace listener attaches. Only then can the first test start.

From 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.

RecordReading
Seven capability entities: Playwright, Data, Sheets, GraphQL, REST, ASP.NET Core, SQLrecorded when the host started, before the test's setup opened
Run start 18:37:08.072Z, the test's setup opens 18:37:09.060Zthe host was alive about 1.0 second before the test
Release · messaging:broker, 0.3 msthe host releases a run piece after the test
Release · readiness:application:Northstar web, 0.0 msthe readiness probe is a run resource
Release · application:loopback:Northstar web, 5.0 msthe listener the browser journey follows
The report's Resources section lists the run pieces as Registeredthe 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?

Verify
Download l1-first-journey.prototrace, open it in the viewer, and read the run layer: the capabilities at its top, the releases at its end.

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.

Keep exploring​