Give each test its own state
The problem
A test that assumes a record exists can depend on earlier tests or leftover data. The sample's state drill requests the project prj_1 without creating it. The application answers 404, while the test expects 200. The drill is one of four in the failure tour.
The fix creates a project in the test's own tenant, then checks that its list contains that project. You can follow the source excerpts and saved trace without running the sample.
Do it
1. Give the test its own tenant
In this sample, a tenant groups one customer's data. NorthstarTenantAttribute creates one during test setup. NorthstarMember includes that attribute, so the state drill and its fix both receive a tenant:
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 registered provisioner decides how the tenant is created.
- Include the test id in the name
UniqueName returns "northstar-<test id>". A name is not an access boundary.
- Keep the tenant details in context
The ids and owner credentials stay with this test.
samples/Northstar.ProtoTest/NorthstarAttributes.cs.2. Create the data inside it, and read it back
EachTenantSeesOnlyItsOwnProjects creates a project, then 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 in this test's tenant
The provisioner uses the signed-in member's tenant.
- Read the same tenant
The member's token makes the application list only this tenant's projects.
samples/Northstar.ProtoTest/FailureDrills.cs. The drill next to it read prj_1.The omitted NUnit assertions check TotalCount == 1, then compare the returned project's id and name with the values created above. Those checks prove that the list contains the expected project in this run.
3. Compare the drill and the fix
| The drill | The fix | |
|---|---|---|
| Before the read | a tenant exists, but the test creates no project | Create · CreateProjectRequest, in the test's tenant |
| The call | GET /api/v1/projects/prj_1, HTTP 404 | GET /api/v1/projects, HTTP 200 |
| The check | expected 200, got 404 | the list holds one project, the one this test created |
4. Find the creation in the trace
Download l0-state-fix.prototrace and open it in the viewer. Select EachTenantSeesOnlyItsOwnProjects. The Steps tab shows its operations phase by phase: Setup before the test body (folded into one line, so open it), then Execution for the body. The first word of each kind, such as data or http, says what sort of step it is:
| Layer | Entry | What it shows |
|---|---|---|
| Setup | data.create ProvisionTenantRequest, 149.6 ms | the tenant northstar-553135000001 exists before the body runs |
| Setup | attribute.before SignedInAs, then auth.user.sign-in | the test declares its identity |
| Execution | data.create CreateProjectRequest, 100.2 ms | the project, in the test's own tenant |
| Execution | http.request REST GET /api/v1/projects, 72.6 ms, HTTP 200 | the read the check judged |
Open the response attachment to see totalCount: 1 and the project named own-553135000001.
What happened
The test arranged its own tenant and project. Its authenticated request selected that tenant, and the application limited the list to that tenant's projects. The name helped identify the record, but did not provide the access boundary.
This holds under parallel runs too: each test keeps its own tenant credentials in its own context.
Cleanup is a separate step. Northstar's tenant provisioner returns a cleanup that deletes the tenant, and ProtoTest registers it as a test-owned resource. Lesson 6, What a test leaves behind, follows it.
Check yourself
Why does EachTenantSeesOnlyItsOwnProjects find exactly one project, even when other tests run at the same time?
Setup creates a tenant, and the test creates one project inside it. Its authenticated list request reads that tenant's projects. Other tests using their own tenant credentials do not add projects to this tenant.
The assertions check the count, id and name. A test id in the name alone would not make a shared tenant safe.
Remember
- Arrange the data a test needs and check the returned identity. Do not assume a fixed id already exists.
- Use separate tenant state and credentials for isolation. Naming and cleanup are separate responsibilities.
- The setup layer of the trace shows what was provisioned. The execution layer shows what the test did with it.
Go deeper
- Parallel safety: how per-test ownership and scoped reads keep concurrent tests independent.
- Next: What a test leaves behind.