Create test data with a provisioner
The problem
A test that inserts rows directly skips the product's rules. A copied fixture drifts from its endpoint.
A provisioner creates an object through the door you choose and returns what the system gave back. You write it once, and every test asks for data by type.
Do it
1. Read the shape
A provisioner implements one method. It takes a request and returns the created value and its identity:
public interface IProtoDataProvisioner<TInput, TResult>
{
ValueTask<ProtoDataProvisioningResult<TResult>> CreateAsync(
TInput value,
ProtoDataProvisioningContext context,
CancellationToken cancellationToken);
}
The sample's API provisioners share one base class, so each names only the URL, the body and the id:
1public abstract class NorthstarApiProvisioner<TRequest, TResponse> : IProtoDataProvisioner<TRequest, TResponse>2{3protected abstract string Url { get; }45protected abstract object Body(TRequest value);67protected virtual object? RouteValues(TRequest value) => null;89protected abstract string IdOf(TResponse response);1011public async ValueTask<ProtoDataProvisioningResult<TResponse>> CreateAsync(12TRequest value,13ProtoDataProvisioningContext context,14CancellationToken cancellationToken)15{16using var response = await context.Execution.Rest()17.Body(Body(value))18.PostAsync(Url, RouteValues(value), ct: cancellationToken);19response.Should.HaveHttpStatus(HttpStatusCode.Created);20var created = response.ReadAsJson<TResponse>()21?? throw new InvalidOperationException(22$"The sample app returned no provisioned {typeof(TResponse).Name}.");23return new ProtoDataProvisioningResult<TResponse>(created, IdOf(created));24}25}
- Name the identity
A later call finds the same value by this id.
- Use the test's clients
context.Execution is the test that asked for the fixture.
- Require the contract
A creation without 201 fails the test here.
samples/Northstar.ProtoTest/Provisioners/.2. Register it
One line on the host builder names the request, the result and the implementation:
builder.AddDataProvisioner<InviteMemberRequest, MembershipResponse, NorthstarMemberProvisioner>();
A defaults module fills values that every fixture of a kind shares, so a test only sets what it cares about:
public sealed class NorthstarDataDefaults : IProtoDataDefaultsModule
{
public void Configure(ProtoDataConfiguration data)
{
data.For<ProvisionTenantRequest>()
.Default(request => request.PlanId, PlanIds.Free);
data.For<InviteMemberRequest>()
.Default(
request => request.Email,
context => $"member-{context.TestId}-{context.ObjectSequence:D4}@example.test");
}
}
3. Use it and read the chain
A test creates a project with the extension method the sample keeps for it:
var project = await Proto.Context.Data().CreateProjectAsync($"provision-{Proto.Context.TestId}");
One call writes a chain of entries. This one is from the first journey's trace, l1-first-journey.prototrace, where the tenant attribute from the last lesson made the same kind of call:
| Entry | Reading |
|---|---|
Create · ProvisionTenantRequest, 137.0 ms | the request arrived and found its provisioner |
Build · ProvisionTenantRequest, 3.5 ms | the defaults built the value to send |
Provision · ProvisionTenantRequest → TenantResponse, 132.0 ms | the provisioner made the call |
Release · data:TenantResponse:1, 10.3 ms, then Cleanup · TenantResponse | teardown released the value and cleaned it up |
The test knew no port or route. The registration chose the implementation, and the trace names it.
What happened
The provisioner holds how to create a fixture, in one named place. The test asks for a value by type, and the trace shows which implementation answered and how long each step took.
Check yourself
A provisioner returns a result with an identity and, optionally, a cleanup. What does the identity let another call do, and what happens to the cleanup when the test ends?
The identity lets a later call find the same value again, through Ref<T>. The cleanup runs at teardown, in reverse creation order, as a data.cleanup operation.
Remember
- A provisioner turns a built request into a created value and reports its identity.
- A defaults module fills the values every fixture of a kind shares.
- One data call writes a chain of entries: create, build, provision, then release and cleanup at teardown.
Next: model a screen as a page object.
Go deeper
- Provisioners: the contract, the identity map, cleanup and trace attributes.