Keep state per test and clean it up
The state drill from Level 0 reads a project id that no test in the run created (prj_1, a fixed id that belongs to no tenant), and the request 404s. That drill is one of the four in the failure tour. The fix creates its own state and reads only that. This lesson reads the fix in the trace, including what the run owns and removes.
- Provision state in setup under a name that carries the test id.
- Read only the state the test created.
- Find the cleanup and the release that prove the state was removed.
- Wait for readiness, not for time (lesson 2).
- Nothing installed. The archives are on this site.
The scenario
A test that reads state it did not create depends on whatever ran before it. The fix for the state question is short: create the data the test needs, read it, and let teardown remove it.
In the sample the unit of isolation is the tenant. An attribute provisions one per test, the test works inside it, and teardown deletes it. The project the test creates lives inside that tenant and goes with it.
Where the tenant comes from
The attribute that provisions it uses a name from the test:
1public override async Task BeforeTestAsync(ProtoExecutionContext context)2{3var tenant = await context.Data()4.For<ProvisionTenantRequest>()5.With(request => request.Name, context.UniqueName("northstar"))6.With(request => request.PlanId, PlanId)7.CreateAsync<TenantResponse>();8context.SetContext(new NorthstarOrganizationContext(9tenant.Tenant,10tenant.OrganizationId,11tenant.OwnerEmail,12tenant.OwnerToken,13tenant.ApiBaseUrl));14}
- Provision through the data client
The request says what the test needs; the registered provisioner decides how it is created.
- Name it from the test id
UniqueName turns "northstar" into "northstar-<test id>", so no other test can share the record.
- Hand the result to the test
The provisioned tenant lands in the execution context, where the test and its clients read it.
samples/Northstar.ProtoTest/NorthstarAttributes.cs. The provisioner returns the tenant with a disposer, which is what makes the run own it.The journey that reads it
EachTenantSeesOnlyItsOwnProjects creates a project and lists the projects its tenant can see:
1var name = $"own-{Proto.Context.TestId}";2var project = await Proto.Context.Data().CreateProjectAsync(name);34using var response = await Proto.Context.Rest().GetAsync("/api/v1/projects");5var page = response6.Should.HaveHttpStatus(HttpStatusCode.OK)7.ReadRequired<CursorPage<ProjectResponse>>();
- Create with the test id
The name carries TestId, so the record belongs to this test even though the project name itself is ordinary.
- Read only the own tenant
The list call runs inside the provisioned tenant, so it returns the one project this test created.
samples/Northstar.ProtoTest/FailureDrills.cs. The drill next to it read prj_1, a fixed id that belonged to no test in the run.The list holds one project, and it is the one the test just created. The drill next to it read prj_1, a fixed id that belonged to no test in the run, and the check failed on the 404 the application answered.
The two layers to read
From l0-state-fix.prototrace:
| Layer | Entry | What it proves |
|---|---|---|
| Setup | data.create ProvisionTenantRequest, 149.6 ms, then data.provision, 144.6 ms | the tenant exists before the body runs, named northstar-553135000001 in this recording |
| Setup | attribute.before SignedInAs, then its auth.user.sign-in event | the member acts inside that tenant |
| Execution | data.create CreateProjectRequest, 100.2 ms | the project is created under the test's own tenant |
| Execution | http.request REST GET /api/v1/projects, 72.6 ms, HTTP 200 | the read the check judged |
| Teardown | data.cleanup TenantResponse, 9.0 ms, and resource.release of data:TenantResponse:1 | the owned state is removed, and the release is recorded |
The value list tells the same story: the tenant item records owned: true, and the project is a value the test read. One cleanup covers both.
What a leak would look like
If the cleanup were missing, the teardown layer would hold no data.cleanup and no release for the tenant, and the record would stay in the store. The run prefix changes per run, so the next run provisions a fresh tenant. The leak still grows the store. It collides when a suite fixes its run prefix or points at a shared environment. The trace is where the leak is visible before that happens.
Checkpoint
The trace lists the tenant as an owned value and records its cleanup and release. The project the test created is not released on its own. Where does it go?
data.cleanup entry for the tenant and none for the project: the test asked for the project, and the tenant owns the store it lives in.What you learned
- A test provisions the state it reads, under a name built from its own test id.
- The owned resource is the tenant; the data inside it is removed with it.
- Teardown records each cleanup and release, so a missing one is visible in the same place.