Testing gathers evidence about specific failure risks. Unit tests give fast feedback on narrow behavior, integration tests exercise real boundaries and wiring, and the testing pyramid models the cost of moving an assertion through more of the system. TDD adds a short test-first feedback loop when that loop helps shape an interface or behavior.

API Testing by Failure Mode

“API test” describes a boundary, not one test type. The useful test is the narrowest one that can faithfully expose the risk. A schema check catches a renamed field without booting the service. An integration test proves database and authentication wiring. A load test measures a latency budget under concurrency. Applying every category to every endpoint adds runtime and maintenance without increasing confidence in a named failure mode.

Assume POST /orders accepts an idempotency key, validates inventory, charges a payment provider, and returns 201 Created with an order resource.

RiskNarrowest useful testConcrete assertionRelease signal
Contract driftSchema or consumer contract testRequired fields, status codes, media type, and error shape still match the published OpenAPI contractRun on every change to the handler or contract
Broken integrationIn-process API test with real persistence and test doubles only at external networksA valid request commits one order. An invalid token returns 401. A provider timeout rolls back or leaves a recoverable stateRun in pull requests and before deployment
Regression in a known incidentFocused test at the lowest reproducing layerReplaying the same idempotency key returns the first result and creates no second chargeAdd with the fix and keep permanently
Capacity or latency collapseLoad and stress tests in a production-like environmentAt 500 requests per second, p95 stays below the budget and the error rate remains bounded. Above the limit, backpressure is controlledRun before capacity-sensitive releases and on a schedule
Security boundary failureAuthorization, input, rate-limit, and abuse-case testsOne tenant cannot read another tenant’s order. Oversized or malicious input is rejected without leaking internalsRun for every exposed operation and threat-model change
Parser or state-machine edge caseProperty-based or fuzz testGenerated JSON never crashes the process. Accepted inputs preserve the order invariantRun continuously on parsers and complex validation

Smoke tests answer a smaller question: is the deployed API reachable, and does one critical path still work? They are deployment checks rather than substitutes for the risk-focused tests above. Browser tests belong where rendering, client routing, or the browser-to-API interaction is part of the contract.

A Minimal ASP.NET Core Integration Test

WebApplicationFactory<TEntryPoint> hosts the ASP.NET Core application through an in-process test server. Keep routing, middleware, serialization, authentication, and persistence as real as the target risk requires. Replace only boundaries the test does not own or cannot run deterministically.

public sealed class OrderApiTests(OrderApiFactory factory)
    : IClassFixture<OrderApiFactory>
{
    [Fact]
    public async Task ReplayingAnIdempotencyKeyCreatesOneOrder()
    {
        var client = factory.CreateClient();
        client.DefaultRequestHeaders.Add("Idempotency-Key", "order-42");
 
        var first = await client.PostAsJsonAsync("/orders", new { Sku = "book", Quantity = 1 });
        var replay = await client.PostAsJsonAsync("/orders", new { Sku = "book", Quantity = 1 });
 
        first.StatusCode.Should().Be(HttpStatusCode.Created);
        replay.StatusCode.Should().Be(HttpStatusCode.Created);
        factory.Orders.Count(order => order.IdempotencyKey == "order-42").Should().Be(1);
    }
}

This test earns its higher setup cost because the invariant crosses HTTP binding, middleware, application logic, and persistence. A pure price calculation exposes no such boundary and belongs in a faster unit test.

References

4 items under this folder.