Assert controller responses

TestScenarioResult.Should() returns framework-independent response assertions for status, success, headers, and structural JSON bodies. Use them when a test should report the HTTP contract in terms of expected and actual values without depending on an assertion-library adapter.

Assert status and headers

[Fact]
public async Task Response_assertions_accept_status_and_response_or_content_headers()
{
    using var response = new HttpResponseMessage(HttpStatusCode.OK)
    {
        Content = new StringContent("{}", Encoding.UTF8, "application/json")
    };
    response.Headers.Add("X-Correlation-Id", "test-42");
    using var result = await ExecuteAsync(response);

    var assertions = result.Should();

    Assert.Same(assertions, assertions
        .HaveStatusCode(HttpStatusCode.OK)
        .BeSuccessful()
        .HaveHeader("X-Correlation-Id")
        .HaveHeader("X-Correlation-Id", "test-42")
        .HaveHeader("Content-Type"));
}

HaveHeader searches both response headers and content headers. Use the value overload when the exact header value is part of the contract. Avoid asserting volatile transport headers unless the application intentionally controls them.

BeSuccessful accepts status codes in the successful HTTP range. Use HaveStatusCode when one specific status is required.

Assert a JSON body

HaveJsonBodyAsync serializes the expected value and compares JSON structurally. Property order and insignificant whitespace do not affect the result. Pass JsonSerializerOptions when application naming, converters, or number handling differs from defaults.

await result.Should()
    .HaveStatusCode(HttpStatusCode.OK)
    .HaveJsonBodyAsync(
        new OrderResponse(42, "Ready"),
        cancellationToken: cancellationToken);

Invalid response JSON produces TestHttpResponseVerificationException with the parse failure as its inner exception. A structural mismatch includes formatted expected and actual JSON. Keep bodies small enough for useful diagnostics; use snapshots when the whole response is intentionally a reviewed artifact.

Understand failure diagnostics

Assertions throw TestHttpResponseVerificationException. Stable messages name the assertion and include the expected and actual status, header, or JSON content:

[Fact]
public async Task Response_assertions_describe_status_and_header_failures()
{
    using var response = new HttpResponseMessage(HttpStatusCode.BadRequest);
    response.Headers.Add("X-Mode", "actual");
    using var result = await ExecuteAsync(response);
    var assertions = result.Should();

    var status = Assert.Throws<TestHttpResponseVerificationException>(
        () => assertions.HaveStatusCode(HttpStatusCode.OK));
    var success = Assert.Throws<TestHttpResponseVerificationException>(
        () => assertions.BeSuccessful());
    var missing = Assert.Throws<TestHttpResponseVerificationException>(
        () => assertions.HaveHeader("X-Missing"));
    var missingWithValue = Assert.Throws<TestHttpResponseVerificationException>(
        () => assertions.HaveHeader("X-Missing", "expected"));
    var wrongValue = Assert.Throws<TestHttpResponseVerificationException>(
        () => assertions.HaveHeader("X-Mode", "expected"));

    Assert.Contains("200 (OK)", status.Message);
    Assert.Contains("400 (BadRequest)", status.Message);
    Assert.Contains("successful", success.Message);
    Assert.Contains("X-Missing", missing.Message);
    Assert.Contains("X-Missing", missingWithValue.Message);
    Assert.Contains("expected", wrongValue.Message);
    Assert.Contains("actual", wrongValue.Message);
    Assert.Throws<ArgumentException>(() => assertions.HaveHeader(" "));
    Assert.Throws<ArgumentNullException>(() => assertions.HaveHeader("X-Mode", null!));
}

Tests of diagnostics should assert stable fields rather than the complete message. This allows wording and formatting to improve without breaking consumers that do not depend on exact text.

Ownership and cancellation

The scenario result owns the client and response produced by ExecuteAsync; dispose it after the last assertion. Direct-client tests continue to own and dispose their client and response separately. Pass the test cancellation token to request and asynchronous body assertion methods.

When to use snapshots

Use direct response assertions for focused behavioral contracts and snapshots for larger payloads whose changes should be reviewed as a whole. Snapshot options provide redaction, scrubbers, update modes, and obsolete-file detection. See built-in snapshot testing.