Skip to main content

Configuration

Most ProtoTest options can be set in code, in configuration files, or both. One suite can run in-process on a laptop and against a deployed environment in CI, with nothing but a different settings file.

Which value wins​

Values are applied in this order, and the later wins:

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

So code sets the defaults for the suite, and an environment overrides them without a rebuild.

The one exception is a base address: a URL passed directly to AddClient("Api", "https://...") wins over the client's base address under ProtoTest:Applications:{app}. That base address is the application's BaseUrl joined with Endpoints:{client} when that endpoint is configured. Leave it out of 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. Add whatever sources you use:

// 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. Make sure JSON files are copied to the output directory:

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

With environment variables, use __ for the section separator: ProtoTest__Applications__Api__BaseUrl.

You can also supply values in code, which the sample suite does 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 configuration through Proto.Context.Configuration.

Host options (code only)​

Set two option sets in code while the host is built. They do not bind from configuration, so 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 && EmbedSourcesread 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 entirely, register your own IProtoTestIdGenerator with ConfigureServices.

Repeated registration​

As a rule, register infrastructure once. Clients compose and config callbacks accumulate. Hooks and gates are the exception: each call adds another one. A repeated Add... is safe by design, but 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; the known configuration section is bound over the result
AddSql, AddSheets, AddEntityFrameworkCorethe first call wins; later calls are no-ops
AddDatacomposes onto one registry; 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, and 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: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)

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

A renamed section keeps its old key working as a deprecated fallback. See Migrating from 1.0 for the gRPC rename.

Configured only in code: tracing (ConfigureTracing), test ids (ConfigureTestIds) and data defaults (AddData). Those callbacks run while the host is being built, before configuration exists, so they cannot read IConfiguration. If a value needs to vary per environment, read it yourself, for example trace.OutputPath = Environment.GetEnvironmentVariable("TRACE_PATH") ?? "TestResults/run.prototrace";. Inside tests, hooks and attributes, context.Configuration has 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.