Module 1 · HTTP contracts and realistic test boundaries · Lesson 1 of 4
Build an HTTP contract test that can catch a real regression
Watch
Start with a failure the customer can observe
Suppose POST /orders must reject a quantity of zero, create one order for a valid request, return its identifier, and expose that order through GET /orders/{id}. A test that only asserts a controller method was called cannot establish this contract.
This lesson develops a test design and review checklist. It does not supply the application, database fixture, package manifest or runnable test project. The scenarios describe evidence to collect; they are not records of an executed suite.
Write a behavior table before choosing test helpers:
- Valid request: expected status, response schema and one persisted order.
- Invalid quantity: validation response and no persisted order.
- Unknown product: agreed error response and no order.
- Duplicate idempotency key: agreed replay behavior and no second order.
- Unauthorized caller: denied access and no state change.
The exact HTTP status codes belong to your API contract. Do not copy a status code from a tutorial without deciding what it means for your service.
Use the application pipeline
WebApplicationFactory can start the application under a test host and provide an HttpClient. This exercises routing, middleware, binding and serialization as well as endpoint logic. The test project references the application and a compatible Microsoft.AspNetCore.Mvc.Testing package. Some hosting and networking behaviors still require a real network or browser test.
Arrange, act, assert
Arrange only the state needed for the scenario: a known product, an isolated identity and a unique correlation value. Act through the HTTP boundary. Assert the response status and relevant fields, then read persisted state through an independent scope.
Avoid using the exact same in-memory object used by the handler as the sole evidence of persistence. A separate read helps detect missing SaveChanges or transaction mistakes.
The redirect trap
A client may follow a redirect to a login page and return a final 200 response. For a denial-path test, disable automatic redirects or inspect the original response explicitly. Otherwise “success” may mean that HTML for a login page was returned, not that the API accepted the request.
Version note: ASP.NET Core 10 known API endpoints using cookie authentication normally return 401/403 for denied requests. Login redirects remain relevant to older apps, non-API endpoints and custom behavior. Test your actual contract.
Worked review
Weak assertion: response.IsSuccessStatusCode is true. Stronger assertion for the chosen creation contract: status is 201, Location identifies the created resource, the response identifier is nonempty, and a fresh read finds exactly one order with the requested quantity.
Do not assert unstable generated timestamps to the millisecond. Assert the contract's actual precision or inject a controllable clock where time determines behavior.
Exercise
Design a test for a missing required field. State the input, expected status, expected error field and expected database row count. Then change the endpoint to accept the invalid request: your test should fail for a meaningful reason.
Answers to common review questions
- Does an HTTP test prove TLS configuration? Not when traffic stays inside an in-memory test server.
- Does 100% line coverage prove the contract? No; execution is not a correctness assertion.
- Should every business-rule permutation use a full host? Usually keep pure rule combinations in fast unit tests and reserve integration tests for boundaries and wiring.
Watch and apply: free supplementary videos
Benjamin Day's Integration Testing ASP.NET Core with Web Application Factory: Demo 1 introduces xUnit, a factory-created HTTP client, and GET/form POST tests. Open the Video tab or watch on the creator's YouTube channel. This is an MVC/HTML demonstration; strengthen its generic success checks with this lesson's exact API status, media type, fields and persistence checks. Use async Task tests and dispose the factory/client or own them in a correctly scoped fixture. The linked repository currently targets .NET 9 and xUnit 2; its async void example is unsuitable for xUnit v3.
Creator's explanation and code · xUnit guidance on async void
For the database boundary, Milan Jovanović's The Best Way To Use Docker For Integration Testing In .NET is another freely available supplement:
The video is introduced as a PostgreSQL walkthrough; the creator's companion article and sample use SQL Server. The article sample calls MediatR directly, so it does not establish HTTP routing, serialization or authorization coverage. It targets older .NET 7/Testcontainers 3.4 patterns: use compatible current packages, pin database images, initialize the schema and plan per-test data isolation. A shared container alone does not isolate records. See the current official Testcontainers ASP.NET Core example and best practices.
Both videos are optional, original-creator resources available without Full Access. These recommendations were reviewed against creator pages and source samples, not video playback. After the introduction, write down the exact response fields and the independent state observation your API test requires. No sample project is supplied or executed by this lesson.
Response-shape checks that catch quiet regressions
A DTO can ignore an unexpected JSON property. If a field must never be exposed, inspect the actual JSON and assert its absence. For an unordered collection, compare the complete expected items and multiplicity; do not accidentally allow duplicates by comparing sets alone. Preserve ordering checks when order is part of the API contract.
Analogy
Imagine marking a parcel, following it through a depot, then checking both its receipt and its tracking record. Asking one worker whether they touched it answers a much smaller question. Use this picture to choose observable checkpoints. The story does not establish which parts of your actual test are running; that must be recorded separately.
Quick reference
- Send the public HTTP method and path through the configured application pipeline.
- Assert exact status, relevant headers, response shape, and meaningful values.
- Disable automatic redirects when testing the original redirect response.
- Arrange explicit, isolated state; a new HttpClient does not reset a database.
- Verify writes through a fresh scope and DbContext after the request completes.
- Test rejected input and missing resources, including required absence of side effects.
- Replace dependencies inside the test host and verify the replacement was actually used.