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.
Compose once, enter per test
Section titled “Compose once, enter per test”A Stage comes in two steps:
- Composing builds your containers — this is the expensive part, and you do it once.
- 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();// Once — in TestMain (never init: strict lint configs forbid it).setup, err := mokkit.NewSetup(context.Background(), mocks, app)
// Per test. Cleanup is registered with t automatically.f := setup.Enter[Arrange, Act, Inspect](t)
// Or, when the subject is cheap to build, compose and enter per test in one call.f := mokkit.Enter[Arrange, Act, Inspect](t, mocks, app)Enter opens a stage, registers its release with t.Cleanup, and hands back a mokkit.Fixture typed
with the suite’s phases; a scoped service that implements io.Closer is closed when the stage ends.
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.
What a Stage gives you
Section titled “What a Stage gives you”stage.Arrange(); // → ITestArrange — start the setup chainstage.Act(); // → ITestAct — start the act chainstage.Inspect(); // → ITestInspect — start the observe chain
stage.Execute<TService>(svc => ...); // resolve one service and run itstage.ExecuteAsync<TService, TOut>(svc => ...); // resolve, run, return a resultExecute/ExecuteAsync come in 1-to-4-service arities, so a step can pull several collaborators at once.
f.Arrange() // the suite's Arrange — fails hardf.Act() // the suite's Act — fails hardf.Inspect() // the suite's Inspect — fails soft
f.Of[Buyer]() // the per-test artifact registry, promoted onto the fixturemokkit.Resolve[*SignupService](f.Stage) // resolve straight off the stagemokkit.Fixture[A, C, I] is generic over the three phase types, so the phases come back as the suite’s
own vocabulary; inside a step, h.Resolve[T]() does the resolving. Stages can also be observed —
every step, with phase, name, duration and outcome — which is how the
Allure reporter works.
Your vocabulary verbs are thin wrappers over exactly these calls.
Wiring it to your test framework
Section titled “Wiring it to your test framework”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.
One composition per system-under-test
Section titled “One composition per system-under-test”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.
- Containers & the mock→DI bridge — what goes into
Create, and how mocks reach the real service. - Captures: Capture vs Trapture — how artifacts thread from one phase to the next.