Skip to main content

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 toUse
package setup for some testsa ProtoAttribute
run code around every test or the whole runa hook
give tests a new clienta client initializer plus an extension method
create data in your systema data provisioner
report on what tests didobservations and a collector
write reports somewherea sink
show up in the tracethe 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:

NorthstarScenario.cs2 notes
1public sealed class ScenarioProbeInitializer : IProtoClientInitializer<ScenarioProbe>
2{
3public string Name => "ScenarioProbe";
4
5public Task<bool> TryInitializeAsync(ProtoExecutionContext context)
6{
7context.RegisterClient(new ScenarioProbe(), Name);
8return Task.FromResult(true);
9}
10}
  1. The name is the handle

    A test resolves the client by this name. Returning false lets the next initializer try.

  2. Register a live instance

    This is what makes the client resolve from the context.

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

NorthstarScenario.cs3 notes
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.CorrelationId
9});
  1. Kind: dotted, lowercase

    The viewer labels a kind it does not know by its first segment: Northstar.

  2. Name: for humans

    A verb and a subject, like the built-in entries.

  3. Source: your package

    It separates your entries from the framework's.

The same two calls bracket the scenario: 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:

EntryReading
Initialize · ScenarioProbe (ScenarioProbe), 0.1 msthe initializer registered the client in setup
Before · NorthstarScenarioHook, 5.9 msthe hook wrote the opening event
Publish · <test id>-scenario-summary.json, 0.5 msthe 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?

Verify
Open l1-first-journey.prototrace in the viewer and read the setup and teardown phases, or read the table above.

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​