REST
ProtoTest.Rest gives each test a named HTTP client with JSON shape assertions.
[Application("Api")]
public sealed class OrderTests
{
[ProtoTest]
public async Task Creates_an_order()
{
using var response = await Proto.Context.Rest()
.Body(new { product = "notebook", quantity = 2 })
.PostAsync("/api/orders");
response.Should.HaveHttpStatus(HttpStatusCode.Created);
}
}
Run it with dotnet test. A green run prints Passed Creates_an_order, and the trace lands at TestResults/prototest-{runId}.prototrace with an http.request operation for the call.
Install
dotnet add package ProtoTest.Rest
See Installation for the supported .NET versions.
What it adds
Each test gets a named HTTP client built on IHttpClientFactory, with JSON shape assertions, the shared authentication model, optional request/response capture and endpoint coverage. It brings ProtoTest.Http and ProtoTest.Json with it.
Compose
Register REST on the host, or under an application so its clients share the application's base URL:
builder.AddRest(); // host-level clients only
builder.AddApplication("Api", app => app.AddRest(rest => rest
.AddClient("Api") // the application's default REST client
.AddClient("Billing", endpoint: "BillingApi"))); // joins BaseUrl with Endpoints:BillingApi
Calling AddRest twice does not throw. The shared setup runs once. Each callback still adds its clients.
AddClient overloads
| Overload | Base address |
|---|---|
AddClient(name = null, baseUrl = null, configure = null, endpoint = null) | the explicit baseUrl, else the application's BaseUrl joined with Endpoints:{endpoint} |
AddClient(name, Func<ProtoExecutionContext, Uri> resolver, configure = null) | resolved per request from the running test |
AddClient(name, Func<ProtoExecutionContext, CancellationToken, ValueTask<Uri>> resolver, configure = null) | async per-request resolution |
Omit the name when the application has one REST client. Pass a name only for several targets (Rest("Billing"), [Application("Api", "Rest:Billing")]).
There is no endpoint default: endpoint names the key under ProtoTest:Applications:{application}:Endpoints to append to the application's BaseUrl. Without it the base URL is used as-is: an explicit baseUrl, or the application's BaseUrl. An explicit non-absolute base URL throws ArgumentException.
Base URL from configuration
{
"ProtoTest": {
"Applications": {
"Api": {
"BaseUrl": "https://staging.example.test/",
"Endpoints": { "BillingApi": "/api/billing" }
}
}
}
}
The tasks
The Creates_an_order test above is the whole pattern: build, send with a verb, assert. The pages below cover each step.
Going further
- Authentication -
.Auth<T>(...),.WithoutAuth(), and[Auth<T>]on the class or method: Authentication. - Attachments -
.CaptureAttachments()records sanitized request, response and expected-shape attachments: Attachments and coverage. - Coverage - attach
.AddCollector<RestCoverageCollector>()to the client; OpenAPI coverage consumes the shape assertions' matched paths, and.AddCollector<RestTrafficCoverageCollector>()reports the fields no shape mentioned: Coverage. - Multiple clients - pass
.Rest("Billing"), or bind one for the whole test with[Application("Api", "Rest:Billing")]. - In-process server -
AddAspNetCoreServer<Program>()plus a client with no URL reuses its transport automatically. A configuredBaseUrlwins and the in-process server stays stopped.
Reference: resolution and options
RestRequestBuilder Rest(this ProtoExecutionContext context, string? clientName = null);
Proto.Context.Rest(name) picks the client in this order:
| Step | What it tries |
|---|---|
| 1 | the requested name |
| 2 | the client bound by [Application(…)] for REST |
| 3 | the application's first registered REST client |
| 4 | "Default" |
A requested name first tries its application-qualified form, then the name as given. When exactly one client of the protocol registered that name on another application, the call reaches it: Rest("Api") under a Dashboard-selected test reaches Csms:Api when Dashboard has no Api client. Two applications sharing the name fail naming both qualified candidates, so qualify the call (Rest("Csms:Api")); an already qualified name is exact.
The client's base address is the application's, resolved with the shared precedence: an address a started piece published wins over configuration. A client with no base address and no owner for its address falls back to the application's in-process transport, rooted at the endpoint the client registered, then the requested name, looking up ProtoTest:Applications:{application}:Endpoints:{name}. A per-test resolver beats HttpClient.BaseAddress at request time.
A client whose address resolves is built over a test-owned handler, so its cookie jar carries only that test's session. Parallel tests never share sign-in state. If nothing resolves, the call throws InvalidOperationException listing the protocol's registered client names.
| Section | Key | Default |
|---|---|---|
ProtoTest:Rest:Responses | MaxResponseBodyBytes | 10485760 (10 MiB) |
MaxDiagnosticBodyLength | 65536 | |
ProtoTest:Rest:Attachments | CaptureRequestBodies, CaptureResponses, CaptureExpectedShapes | true |
RedactSensitiveData | true | |
MaxDiagnosticBodyLength | 65536 | |
SensitiveHeaders | Authorization, Proxy-Authorization, Cookie, Set-Cookie, X-Api-Key | |
SensitiveQueryParameters | password, token, access_token, refresh_token, secret, apiKey, api_key, authorization, cookie, connectionString, clientSecret, client_secret, id_token, key | |
SensitiveJsonProperties | password, token, access_token, refresh_token, secret, apiKey, api_key, authorization, cookie, connectionString, clientSecret, client_secret, id_token |
Set them in code or configuration. Configuration binds last, so it wins over code. Both option types are shared with GraphQL: each protocol owns its own keyed instance and section. When a protocol names no section of its own, the types fall back to the shared ProtoTest:Http:Responses and ProtoTest:Http:Attachments sections; every protocol here names one.
In the trace and coverage
http.request REST · POST /api/orders · 201 Created
├─ assert.http.status (expected and actual agree)
├─ assert.json.shape (5 properties matched at once)
└─ http.response (observation: status, sanitized body and headers, duration)
Each request records an http.request operation (REST · {METHOD} {route}) under the protocol-scoped client entity (client:System.Net.Http.HttpClient:Rest:{target}), with the method, route, sanitized URL, header count and response status. Assertions record assert.http.status, assert.http.content_type, assert.http.header, assert.http.cookie, assert.http.redirect_location and assert.json.shape as children of that request. Observations: http.response for every response (method, route template, status, sanitized body and headers, duration), http.failure when sending or URI building fails, and http.contract.shape when a shape assertion matches. RestCoverageCollector turns http.response observations into REST coverage items; RestTrafficCoverageCollector consumes http.response and http.contract.shape for its observed-but-unasserted section. See ProtoTrace and Coverage.
Skip
[RequiresCapability(ProtoCapabilityKinds.Protocol, CapabilityName = "REST")]
Limits
| Limit | Matters when | Severity |
|---|---|---|
Shape assertions are positive-only; Should / ShouldNot expose the status, content type, header, cookie and redirect-location assertions, and MatchShape is the only positive-only member. | Asserting that a field is absent or that a value differs. | The test cannot express it. |
| Response bodies are always buffered whole in memory; there is no streaming read API. | Downloading large payloads. | Memory grows with the body size. |
Route tokens are {name} over ASCII letters, digits and _. | Routes use other characters. | The route does not match. |
Registering the same client name twice keeps the first registration; TryAddResponseOptions is first-wins for the keyed instance, though configuration callbacks compose. | Overlapping registrations compose the same client name. | The later registration is ignored. |
| No retry or resilience-policy layer. | Calling a flaky endpoint. | A transient failure fails the test. |
Next
- Building requests - verbs, route templates, bodies and headers.
- Responses and assertions - status, JSON shapes and typed reads.
- Authentication - per-request, per-class and composed authenticators, and the built-in test user.
- Attachments and coverage - what gets captured, redacted and reported.
The same flows live in the sample: ProjectsJourney.cs and Setup.cs.