Skip to main content

Write an integration

Your code can add what a package adds. That means a client the context resolves, a hook around the test, or entries in the trace. The sample's correlation layer is a real integration of about fifty lines, and it is the model for this lesson.

Level 6, lesson 3About 10 minutes
By the end
  • Pick the extension point your feature needs from the map.
  • Follow a custom client from its initializer to its context call.
  • Write your own kind, name and source into the trace correctly.
Before you start
  • Provisioners and page objects (lesson 2).
  • The sample cloned and open in an editor.

The scenario​

A company client, a fixture loader, a file assertion: each is an integration, and each should feel native. Native means the test resolves it from the context, the run records it, and the failure message names it.

The sample's scenario layer does exactly that: a custom client, an initializer, a hook, an attachment and two trace events. It is small enough to read in one sitting.

What you want, and what to use​

The extending page keeps this map. Read it as a menu, not a course:

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

The sample's scenario layer uses three of them at once: a client, its initializer, and a hook that writes the trace and the attachment.

The custom client​

The probe is a plain class with a list of milestones. The initializer is what registers it with the context:

NorthstarScenario.cs3 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. One initializer per client type

    The interface is generic over the client, so the host knows what it is creating.

  2. The name is the handle

    A test resolves the client by this name. Returning false means not me: the next initializer in the group is tried, and a group with no winner fails setup with a named message.

  3. Register a live instance

    The instance belongs to the run. Registering it here is what makes context.Client resolve instead of throwing.

From samples/Northstar.ProtoTest/NorthstarScenario.cs.

Two registrations wire it up, one for the initializer and one for the hook:

public static IProtoHostBuilder AddNorthstarTestSupport(this IProtoHostBuilder builder)
{
builder.ConfigureServices(services => services.AddSingleton<IProtoClientInitializer, ScenarioProbeInitializer>());
return builder.AddTestHook<NorthstarScenarioHook>();
}

The extension method is how the sample's Setup.cs stays readable: the integration owns its registrations, and the composition mentions one line.

The trace entry​

The hook writes the correlation and the milestone trail. This is the event that opens a scenario:

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 groups by the prefix before the first dot, so northstar events get their own category without a viewer release.

  2. Name: for humans

    The built-ins use AREA, verb, subject. Keep the same shape so a reader can scan the execution layer.

  3. Source: your package

    The source names what wrote the entry, which is how a reader tells framework entries from yours.

The same two calls bracket the scenario: northstar.scenario.begin and northstar.scenario.end, with a duration attribute on the closing one.

The conventions are short and worth following:

  • 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. Disposing it without completion records Unknown.

Your turn: one milestone​

The probe is public, so a test can mark its own step. Add this to the first journey from lesson 1, under the [RunNote] you already added; if you removed that file, Write your first test recreates it, and Write your own attribute adds the note.

Proto.Context.Client<ScenarioProbe>("ScenarioProbe").Mark("first-milestone");

Run the filter and open the trace. The teardown publishes the scenario summary as an attachment. Open it and read the milestones:

{"CorrelationId":"scenario-416387000001-6406e159a16243e0beb564c28105893f","TestName":"Northstar.ProtoTest.ProjectsJourney.CreatingAProjectReturnsIt","DurationMs":349.127,"Milestones":["scenario-started","scenario-completed"]}

The committed archive for the first journey, l1-first-journey.prototrace, has the same shape, with the sample's own two milestones. Beside it, the trace holds:

EntryReading
Initialize · ScenarioProbe (ScenarioProbe), 0.1 msthe initializer registered the custom client during setup
Before · NorthstarScenarioHook, 5.9 msthe hook ran before the test and wrote the opening event
Publish · <test id>-scenario-summary.json, 0.5 msthe hook attached the milestone trail at teardown

Your milestone lands in the same attachment, because the client, the hook and the attachment are one integration. That is the whole point: a feature you write behaves like a feature that shipped.

Checkpoint​

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 layers, or read the table at the end of this lesson.

What you learned​

  • There is one extension point per thing you want to add: attribute, hook, client, provisioner, sink or trace entry.
  • A custom client is an initializer the host resolves and a context call that returns it.
  • Kinds are dotted and lowercase, names are for humans, and the source is your package.

Keep exploring​