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:
- the option's default,
- your code (the
configurecallback), - 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 set | In code | In configuration | Which wins |
|---|---|---|---|
| An integration option | rest => rest.MaxResponseBodyBytes = ... | ProtoTest:Rest:Responses:MaxResponseBodyBytes | configuration |
| An application address | AddClient("Api", "https://...") | ProtoTest:Applications:Api:BaseUrl | the code URL |
| An application address | no URL in code | ProtoTest:Applications:Api:BaseUrl | configuration |
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:
| Key | Type | Default | Why code-only |
|---|---|---|---|
RunPrefix | long? | a random six-digit number per host | read at host build |
SequenceDigits | int | 6 (valid 1 to 9) | read at host build |
ConfigureTracing fills a ProtoTraceOptions:
| Key | Type | Default | Why code-only |
|---|---|---|---|
Enabled | bool | true | read at host build |
OutputPath | string? | null, which means TestResults/prototest-{runId}.prototrace | read at host build |
ActivitySources | IList<string> | empty | read at host build |
CaptureSourceLocations | bool | true | read at host build |
EmbedSources | bool | true. 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 call | What a second call does |
|---|---|
AddTestHook, AddRunHook, AddRunGate | adds another hook or gate. There is no dedupe. |
AddCapability | an 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), AddResource | the 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. |
AddClient | clients 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, AddEntityFrameworkCore | the first call wins, and later calls are no-ops |
AddData | composes 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.
AddInfrastructurerejects a resource whoseScopeis notRun. 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 } }
}
}
| Section | Pattern | Documented in |
|---|---|---|
ProtoTest:Applications:{name}:BaseUrl | one per application | the address of a system under test, shared by its HTTP clients and web sessions |
ProtoTest:Applications:{name}:Endpoints:{client} | one per client | a relative path appended to BaseUrl for that client |
ProtoTest:Applications:{name}:OpenApi:Specification | one per application | OpenAPI |
ProtoTest:Applications:{name}:GraphQL:* | one per application | GraphQL, schema coverage |
ProtoTest:Applications:{name}:Grpc:Address | one per application | gRPC |
ProtoTest:Rest:Responses, ProtoTest:Rest:Attachments | one per integration | REST attachments, request limits |
ProtoTest:GraphQL:Responses, ProtoTest:GraphQL:Attachments | one per integration | GraphQL |
ProtoTest:Grpc:Client, ProtoTest:Grpc:Attachments | one per integration | gRPC |
ProtoTest:Messaging, ProtoTest:Messaging:Attachments | one per integration | Messaging |
ProtoTest:Messaging:RabbitMq | one per integration | Messaging (ConnectionString names the broker) |
ProtoTest:Sql | one per integration | SQL (Isolation, SharedWithApplications) |
ProtoTest:Sheets | one per integration | Sheets (IncludeHiddenSheets) |
ProtoTest:Devices:Mqtt, ProtoTest:Devices:Serial, ProtoTest:Devices:Tcp, ProtoTest:Devices:WebSocket | one per integration | Devices |
ProtoTest:Web:Playwright, ProtoTest:Web:Selenium, ProtoTest:Web:Pages | one per integration | Web |
ProtoTest:Reporting:Json, ProtoTest:Reporting:Html | one per integration | Reporting |
ProtoTest:Readiness | one per run | Infrastructure (host probes and containers) |
ProtoTest:CleanupFailures | one per run | Lifecycle (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.