Write an integration
The problem
Your system has something ProtoTest does not know: a company client library, a fixture loader, a file format. You want tests to use it as naturally as Proto.Context.Rest(). That means the test gets it from the context, the run records what it did, and a failure message names it.
An integration is a package that adds this kind of capability. The sample's scenario layer is a real one of about fifty lines: a custom client, a hook around each test, an attachment and two trace events.
Do it
1. Pick the extension point
Read this table as a menu. The scenario layer uses three rows: a client, its initializer, and a hook that writes the trace and the attachment.
| You want to | Use |
|---|---|
| package setup for some tests | a ProtoAttribute |
| run code around every test or the whole run | a hook |
| give tests a new client | a client initializer plus an extension method |
| create data in your system | a data provisioner |
| report on what tests did | observations and a collector |
| write reports somewhere | a sink |
| show up in the trace | the trace writer |
2. Register the custom client
The probe is a plain class that keeps a list of milestones. The initializer registers it with the test context:
1public sealed class ScenarioProbeInitializer : IProtoClientInitializer<ScenarioProbe>2{3public string Name => "ScenarioProbe";45public Task<bool> TryInitializeAsync(ProtoExecutionContext context)6{7context.RegisterClient(new ScenarioProbe(), Name);8return Task.FromResult(true);9}10}
- The name is the handle
A test resolves the client by this name. Returning false lets the next initializer try.
- Register a live instance
This is what makes the client resolve from the context.
samples/Northstar.ProtoTest/NorthstarScenario.cs.Two registrations wire it up, one for the initializer and one for the hook. An extension method keeps them in one place, so the sample's Setup.cs mentions a single line:
public static IProtoHostBuilder AddNorthstarTestSupport(this IProtoHostBuilder builder)
{
builder.ConfigureServices(services => services.AddSingleton<IProtoClientInitializer, ScenarioProbeInitializer>());
return builder.AddTestHook<NorthstarScenarioHook>();
}
3. Write to the trace
The hook opens and closes each scenario with a trace event. This is the opening one:
1context.Trace.WriteEvent(2"northstar.scenario.begin",3"Begin correlated Northstar scenario",4"Northstar.ProtoTest",5outcome: ProtoTraceOutcome.Succeeded,6attributes: new Dictionary<string, string?>7{8["northstar.correlation_id"] = scenario.CorrelationId9});
- Kind: dotted, lowercase
The viewer labels a kind it does not know by its first segment: Northstar.
- Name: for humans
A verb and a subject, like the built-in entries.
- Source: your package
It separates your entries from the framework's.
northstar.scenario.begin and northstar.scenario.end, with a duration attribute on the closing one.Three rules keep your entries tidy:
- Attributes are strings. Keep them small, and never put a secret in one.
- Nesting is automatic. An operation started inside another becomes its child.
- Complete an operation once. A second completion has no effect, and disposing it without completion records
Unknown.
4. Use the client in a test
Add this line to the first journey from Write your own attribute, under the [RunNote] you added there:
Proto.Context.Client<ScenarioProbe>("ScenarioProbe").Mark("first-milestone");
Run the filtered test. At teardown the hook attaches the scenario summary. A real one from the sample:
{"CorrelationId":"scenario-416387000001-6406e159a16243e0beb564c28105893f","TestName":"Northstar.ProtoTest.ProjectsJourney.CreatingAProjectReturnsIt","DurationMs":349.127,"Milestones":["scenario-started","scenario-completed"]}
Your own run lists first-milestone beside the sample's two.
What happened
You did not change the framework. The host resolved your initializer at setup, so Client<ScenarioProbe> found the client. The hook ran around the test, wrote the events and attached the milestone trail.
The committed trace of the first journey, l1-first-journey.prototrace, shows the sample's own entries:
| Entry | Reading |
|---|---|
Initialize · ScenarioProbe (ScenarioProbe), 0.1 ms | the initializer registered the client in setup |
Before · NorthstarScenarioHook, 5.9 ms | the hook wrote the opening event |
Publish · <test id>-scenario-summary.json, 0.5 ms | the hook attached the milestones at teardown |
A feature you write behaves like a feature that shipped.
Check yourself
The first journey runs with the scenario hook registered. Which entry in its trace proves the custom client was registered, and where does your own milestone land?
Initialize · ScenarioProbe (ScenarioProbe) in the setup phase. Your milestone lands beside the sample's two, in the scenario summary the hook attaches at teardown.
Remember
- There is one extension point per thing you add: attribute, hook, client, provisioner, sink or trace entry.
- A custom client is an initializer the host resolves, plus a context call that returns it.
- Kinds are dotted and lowercase, names are for humans, and the source is your package.
Next: swap a dependency for one test.
Go deeper
- Extending ProtoTest: the full extension-point contract, including runners and sinks.