Skip to content

The Stage & lifecycle

The Stage is the runtime a test runs against. It holds the services a test resolves — the real system-under-test plus its (real or mocked) collaborators — and it’s what Arrange, Act and Inspect pull from. Everything else in Mokkit sits on top of it.

A Stage comes in two steps:

  1. Composing builds your containers — this is the expensive part, and you do it once.
  2. Entering a stage returns a fresh, isolated world — you do this once per test; it is released when the test ends.
// Once — usually in a class/collection fixture.
var setup = await TestStageSetup.Create(
new BagContainerBuilder()
.AddInstance(email)
.AddInstance(new SignupService(email)));
// Per test.
var stage = setup.EnterStage();
// ... arrange / act / inspect ...
stage.Dispose();

Each entered stage opens its own scope, so tests are isolated: scoped services are created per stage and released with it. Nothing leaks between tests.

stage.Arrange(); // → ITestArrange — start the setup chain
stage.Act(); // → ITestAct — start the act chain
stage.Inspect(); // → ITestInspect — start the observe chain
stage.Execute<TService>(svc => ...); // resolve one service and run it
stage.ExecuteAsync<TService, TOut>(svc => ...); // resolve, run, return a result

Execute/ExecuteAsync come in 1-to-4-service arities, so a step can pull several collaborators at once.

Your vocabulary verbs are thin wrappers over exactly these calls.

Mokkit is framework-agnostic — the Stage is composed in whatever “run once” hook your runner offers and entered in its “per test” hook. The pattern is identical across xUnit, NUnit, MSTest and TUnit; only the fixture attributes differ.

// xUnit — the composition is an IClassFixture (built once); each test enters a fresh stage.
public abstract class BaseUnitTest<TFixture> : IClassFixture<TFixture>, IDisposable
where TFixture : BaseStageFixture
{
protected BaseUnitTest(TFixture fixture) => Stage = fixture.EnterStage();
protected TestStage Stage { get; }
protected ITestArrange Arrange => Stage.Arrange();
protected ITestAct Act => Stage.Act();
protected ITestInspect Inspect => Stage.Inspect();
public void Dispose() => Stage.Dispose();
}

The same shape on TUnit (which runs on Microsoft.Testing.Platform) — the composition is a [ClassDataSource], and the “per test” hooks are [Before(Test)] / [After(Test)]:

// TUnit — the composition is injected once per class; hooks enter/dispose a fresh stage per test.
public abstract class TUnitTestBase
{
[ClassDataSource<CacheServiceFixture>(Shared = SharedType.PerClass)]
public required CacheServiceFixture Fixture { get; init; }
protected TestStage Stage { get; private set; } = null!;
protected ITestArrange Arrange => Stage.Arrange();
protected ITestInspect Inspect => Stage.Inspect();
[Before(Test)] public void Enter() => Stage = Fixture.EnterStage();
[After(Test)] public void Exit() => Stage.Dispose();
}

Exposing Arrange / Act / Inspect as properties on a base fixture is what lets a test body read as await Arrange.… / await Act.… / await Inspect.… with no ceremony — the same three lines regardless of runner.

Because containers are built once per composition, a service is either the real thing or a mock within a given Stage — not both. So a type that is the system-under-test in one test but a dependency in another needs its own fixture:

One fixture per SUT: its real type, plus mocks for that type’s direct dependencies.

This is a feature, not a limitation — it keeps each test’s composition small and obvious. The project structure page shows how to organise fixtures per feature.