Skip to main content

Configure ProtoTest

You can set most ProtoTest options in code, in configuration files, or in both. This lets one suite run in-process on a laptop and against a deployed environment in CI. Only the settings file changes.

Which value wins​

ProtoTest applies a value from three places, in this order. The later one wins:

  1. the option's default,
  2. your code (the configure callback),
  3. configuration.

So your code sets the suite's defaults, and an environment can override them without a rebuild.

A base address is the one exception. A URL passed directly to AddClient("Api", "https://...") wins over the client's base address in configuration. That configured base address is the application's BaseUrl under ProtoTest:Applications:{app}, joined with Endpoints:{client} when that endpoint is set. Leave the URL out of your code when you want configuration to decide.

You setIn codeIn configurationWhich wins
An integration optionrest => rest.MaxResponseBodyBytes = ...ProtoTest:Rest:Responses:MaxResponseBodyBytesconfiguration
An application addressAddClient("Api", "https://...")ProtoTest:Applications:Api:BaseUrlthe code URL
An application addressno URL in codeProtoTest:Applications:Api:BaseUrlconfiguration

Adding configuration sources​

The host starts with an empty configuration. You add the sources you want:

// dotnet add package Microsoft.Extensions.Configuration.Json
// dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables
using Microsoft.Extensions.Configuration;

builder.ConfigureAppConfiguration(configuration => configuration
.AddJsonFile("appsettings.Test.json", optional: true)
.AddEnvironmentVariables());

AddJsonFile and AddEnvironmentVariables come from the Microsoft.Extensions.Configuration.Json and Microsoft.Extensions.Configuration.EnvironmentVariables packages. Copy the JSON file to the output directory:

<None Update="appsettings.Test.json" CopyToOutputDirectory="PreserveNewest" />

In an environment variable, write __ where the key has a colon: ProtoTest__Applications__Api__BaseUrl.

You can also supply values from code. The sample suite does this for file paths that depend on the build output:

builder.ConfigureAppConfiguration(configuration => configuration.AddInMemoryCollection(
new Dictionary<string, string?>
{
["ProtoTest:Applications:Api:OpenApi:Specification"] =
Path.Combine(AppContext.BaseDirectory, "control-plane.openapi.json")
}));

Tests read the resolved configuration through Proto.Context.Configuration.

Host options (code only)​

Two option sets exist only in code. You set them while the host is built, and they have no configuration section.

ConfigureTestIds fills a ProtoTestIdOptions:

KeyTypeDefaultWhy code-only
RunPrefixlong?a random six-digit number per hostread at host build
SequenceDigitsint6 (valid 1 to 9)read at host build

ConfigureTracing fills a ProtoTraceOptions:

KeyTypeDefaultWhy code-only
Enabledbooltrueread at host build
OutputPathstring?null, which means TestResults/prototest-{runId}.prototraceread at host build
ActivitySourcesIList<string>emptyread at host build
CaptureSourceLocationsbooltrueread at host build
EmbedSourcesbooltrue. Embedding requires CaptureSourceLocations && EmbedSources.read at host build
builder
.ConfigureTestIds(ids => ids.RunPrefix = 42)
.ConfigureTracing(trace =>
{
trace.OutputPath = "TestResults/run.prototrace";
trace.ActivitySources.Add("MyApp.Domain");
});

To replace the id scheme, register your own IProtoTestIdGenerator with ConfigureServices.

Repeated registration​

Show the rules for each registration call

You rarely need this section. It matters when two parts of a setup call the same Add... method. As a rule, register infrastructure once. Clients and configuration callbacks combine. Hooks and gates are the exception, because each call adds another one. What the second call does depends on what it registers:

You callWhat a second call does
AddTestHook, AddRunHook, AddRunGateadds another hook or gate. There is no dedupe.
AddCapabilityan equal descriptor registers once
AddSink<TSink>the first registration of the sink type wins. A repeated generic call appends its configure callback.
AddInfrastructure(piece, keys), AddResourcethe same instance is a no-op (AddInfrastructure also merges the repeated call's settings keys, and AddInfrastructureAlways still forces a start). A different instance under the same id throws "already owned by the run".
AddInfrastructure(name, chain, keys)a repeated target name throws. Add providers to the existing chain instead.
AddClientclients compose and the first registration that initializes for a type and name wins
ConfigureResponses, CaptureAttachments (all protocols)callbacks compose, and the known configuration section is bound over the result
AddSql, AddSheets, AddEntityFrameworkCorethe first call wins, and later calls are no-ops
AddDatacomposes onto one registry, and every call's callback runs
AddCollector<TCollector>the same collector type for the same target registers once

Some repeats fail instead:

  • A different run resource under an existing id throws.
  • AddInfrastructure rejects a resource whose Scope is not Run. It also rejects settings keys on a piece that provides no addresses.
  • A duplicate client type and name throws during setup.

A configure callback that throws is not remembered. A later successful call can still compose the integration.

Everything configurable​

{
"ProtoTest": {
"Applications": {
"ControlPlane": {
"BaseUrl": "https://staging.example.test/",
"Endpoints": { "Api": "/api", "GraphQL": "/graphql" },
"OpenApi": { "Specification": "https://staging.example.test/swagger/v1/swagger.json" }
}
},
"Rest": { "Responses": { "MaxResponseBodyBytes": 10485760 } },
"Sql": { "Isolation": "Transaction" },
"Messaging": { "RabbitMq": { "ConnectionString": "amqp://guest:guest@localhost:5672/" } },
"Web": { "Playwright": { "Browser": "Chromium", "Headless": false } },
"Reporting": { "Json": { "OutputPath": "TestResults/report.json", "Indented": true } }
}
}
SectionPatternDocumented in
ProtoTest:Applications:{name}:BaseUrlone per applicationthe address of a system under test, shared by its HTTP clients and web sessions
ProtoTest:Applications:{name}:Endpoints:{client}one per clienta relative path appended to BaseUrl for that client
ProtoTest:Applications:{name}:OpenApi:Specificationone per applicationOpenAPI
ProtoTest:Applications:{name}:GraphQL:*one per applicationGraphQL, schema coverage
ProtoTest:Applications:{name}:Grpc:Addressone per applicationgRPC
ProtoTest:Rest:Responses, ProtoTest:Rest:Attachmentsone per integrationREST attachments, request limits
ProtoTest:GraphQL:Responses, ProtoTest:GraphQL:Attachmentsone per integrationGraphQL
ProtoTest:Grpc:Client, ProtoTest:Grpc:Attachmentsone per integrationgRPC
ProtoTest:Messaging, ProtoTest:Messaging:Attachmentsone per integrationMessaging
ProtoTest:Messaging:RabbitMqone per integrationMessaging (ConnectionString names the broker)
ProtoTest:Sqlone per integrationSQL (Isolation, SharedWithApplications)
ProtoTest:Sheetsone per integrationSheets (IncludeHiddenSheets)
ProtoTest:Devices:Mqtt, ProtoTest:Devices:Serial, ProtoTest:Devices:Tcp, ProtoTest:Devices:WebSocketone per integrationDevices
ProtoTest:Web:Playwright, ProtoTest:Web:Selenium, ProtoTest:Web:Pagesone per integrationWeb
ProtoTest:Reporting:Json, ProtoTest:Reporting:Htmlone per integrationReporting
ProtoTest:Readinessone per runInfrastructure (host probes and containers)
ProtoTest:CleanupFailuresone per runLifecycle (Fail or Report; Fail reports a passing test as failed when cleanup fails)

ProtoTest:CleanupFailures is read the same way as the other ProtoTest: keys, including ProtoTest:Readiness: only from sources the suite adds with ConfigureAppConfiguration (a JSON file, AddEnvironmentVariables(), or an in-memory collection), and the environment variable is ProtoTest__CleanupFailures.

Each integration reads its options from one section named ProtoTest:<Integration>. Some add a second segment for the area they cover, for example ProtoTest:Rest:Responses or ProtoTest:Grpc:Client.

When a section is renamed, the old key keeps working as a deprecated fallback. See Migrating from 1.0 for the gRPC rename.

Three things are set only in code: tracing (ConfigureTracing), test ids (ConfigureTestIds) and data defaults (AddData). Their callbacks run while the host is being built, before configuration exists, so they cannot read IConfiguration. If a value must vary per environment, read it yourself, for example trace.OutputPath = Environment.GetEnvironmentVariable("TRACE_PATH") ?? "TestResults/run.prototrace";. Inside tests, hooks and attributes, context.Configuration holds the resolved configuration.

Where to next​

  • Environments: the same suite in-process, container-backed or against a published system.
  • Infrastructure: pieces the run starts and the settings they publish.
  • Troubleshooting: when a client or a container does not come up.