Testing is both a verification discipline and a design tool. Unit tests exercise a single class or method in isolation, fast and deterministically; integration tests wire real dependencies together to catch the bugs that isolation hides. The test pyramid is a cost model, not a rule: prefer many cheap unit tests, fewer slow integration tests, and a thin layer of end-to-end checks — inverting it makes the suite slow and flaky. TDD closes the loop by writing the test first, which pressures the code toward small, decoupled units before any implementation exists.

API Testing by Failure Mode

“API test” describes a boundary, not a single test type. Choose the narrowest test that can expose the risk. A contract test catches a renamed field without booting the full system; an integration test proves the database and authentication wiring; a load test proves the latency budget under concurrency. Running nine peer categories on every endpoint creates cost without evidence.

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 is one critical path alive?” They are deployment checks, not substitutes for the risk-focused tests above. UI tests are appropriate only when the browser-to-API interaction is itself the contract under test.

A minimal ASP.NET Core integration test

WebApplicationFactory<TEntryPoint> runs the real middleware and endpoint pipeline in memory. Replace only the external payment boundary, keep serialization, authentication, routing, and persistence behavior as real as the risk requires.

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);
    }
}

The test is worth its higher cost because the invariant crosses HTTP binding, middleware, application logic, and persistence. Pure price calculations should stay in fast unit tests.

Questions

References

3 items under this folder.