Skip to main content

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:

NorthstarApiProvisioner.cs3 notes
1public abstract class NorthstarApiProvisioner<TRequest, TResponse> : IProtoDataProvisioner<TRequest, TResponse>
2{
3protected abstract string Url { get; }
4
5protected abstract object Body(TRequest value);
6
7protected virtual object? RouteValues(TRequest value) => null;
8
9protected abstract string IdOf(TResponse response);
10
11public 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}
  1. Name the identity

    A later call finds the same value by this id.

  2. Use the test's clients

    context.Execution is the test that asked for the fixture.

  3. Require the contract

    A creation without 201 fails the test here.

From 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:

EntryReading
Create · ProvisionTenantRequest, 137.0 msthe request arrived and found its provisioner
Build · ProvisionTenantRequest, 3.5 msthe defaults built the value to send
Provision · ProvisionTenantRequest → TenantResponse, 132.0 msthe provisioner made the call
Release · data:TenantResponse:1, 10.3 ms, then Cleanup · TenantResponseteardown 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?

Verify
Read the provisioner reference, then the release rows of the first journey's trace.

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.