13 — API Testing: Unit, Integration, Functional, Load, Mocking, Contract
My first API "testing" was one big end-to-end suite — slow, flaky, and useless for pinpointing where a failure came from. The structure that fixed it: API testing is a pyramid — unit tests at the base, integration in the middle, functional at the top — and each layer exists because the one below it can't catch the failure mode the upper layer can. [1] Mocking and contract testing are the cross-cutting techniques that keep the layers fast and independent. Trying to test everything end-to-end is slow and flaky; testing only units misses wiring bugs. The pyramid is the balance.
The thread connecting the roadmap nodes is what each layer is for. A unit test catches a logic bug in one handler. An integration test catches a wiring bug between handler and database. A functional test catches an end-to-end behavior bug. A load test catches a capacity bug. A mock keeps a test from depending on a service that isn't ready. A contract test catches a producer/consumer mismatch before deploy. Each one is irreplaceable for its specific failure mode.
Unit testing: each handler in isolation
Unit tests exercise one component — typically one handler or one function — in isolation, with its dependencies mocked or stubbed [2]. The point is to verify the logic of that one piece: given these inputs and these mocked dependencies, does the handler return the right thing? Unit tests are fast (you can run thousands in seconds), precise (a failure points at one function), and they're the foundation of the pyramid because they catch the bulk of logic bugs cheaply.
The discipline: a unit test should fail for exactly one reason, and the failure should point clearly at the function under test. If a unit test requires a database, a real network call, or another service to be up, it isn't a unit test — it's an integration test masquerading as one. Mocking (below) is what keeps unit tests honest.
Integration testing: the wiring
Integration tests verify that components work together [3]. A handler plus a real (or test) database. Two services talking over HTTP. An endpoint that reads from a cache and falls through to the database. These tests catch the bugs unit tests can't — wrong connection strings, schema mismatches, transaction boundaries, serialization issues between layers.
Integration tests are slower than unit tests (they touch real infrastructure), so there are fewer of them in a healthy pyramid. The trap to avoid: an "integration test" that mocks every dependency is really a unit test that's lying about its scope. A real integration test must exercise at least one real seam between components — otherwise it's testing mocks, not integration.
Functional testing: end-to-end behavior
Functional tests validate the API's behavior from the outside — they send real HTTP requests and assert on the real responses, treating the API as a black box [4]. "When I POST /orders with a valid body, I get 201 and the order appears in GET /orders." These tests catch the failure mode neither unit nor integration tests can: the whole flow, end to end, behaving as a consumer would experience it.
Functional tests are at the top of the pyramid because they're the slowest and most expensive — they spin up the whole API and its dependencies. They're also the most valuable per test for confidence, because a passing functional suite means the API actually works from a consumer's perspective. The rule: keep them few and focused on the critical paths (the things that absolutely must work), not on every permutation.
Load testing: capacity under stress
Load testing is functionally different from the layers above — it's not about correctness, it's about capacity [5]. A load test throws realistic or peak traffic at the API and measures how it behaves: throughput, latency percentiles, error rate, where it breaks. This is the test that tells you the API can handle launch day, not just that it returns the right answer for one request.
The discipline I follow: load-test the representative endpoints (the hot paths), ramp traffic until something degrades, and record the ceiling and the bottleneck. A successful load test doesn't just confirm "it works at 10 RPS" — it finds where it stops working, so you know the limit before production finds it for you.
Mocking: testing without the real thing
Mocking is the technique that keeps the layers independent [6]. A mock is a simulated version of a dependency — a fake auth service that always returns "authenticated," a stubbed payment provider that returns "success" — so a test can run without the real dependency being available, configured, or fast.
The use cases that earn mocking its place:
- Unblocking parallel work. The frontend can develop against a mock of the API before the API exists.
- Testing failure paths. Want to see how your handler behaves when the payment provider is down? Mock it to return an error.
- Speed. Mocks are faster than real network calls, keeping unit tests fast.
The trap: a mock is a model of the dependency, and if the model drifts from reality, your tests pass against a fiction. This is exactly the problem contract testing exists to solve.
Contract testing: keeping consumer and provider in sync
Contract testing addresses a specific pain in service-to-service APIs: a consumer (the frontend, another service) and a provider (the API) have agreed on a contract, and both sides evolve independently. Without something checking the agreement, the provider ships a change that breaks the consumer, and no one finds out until deploy (or production) [7].
A contract test formalizes the agreement. The consumer writes down the requests it makes and the responses it expects; the provider verifies it satisfies those expectations. If either side drifts, the contract test fails — before deploy. This is the technique that makes independent deployment of microservices safe; without it, every provider change is a potential silent break for some consumer.
The model I hold: mocking lets you test without the dependency; contract testing checks that your mock and the real dependency still agree. They're a pair. Mock alone and you risk drift; contract-test without mocks and you lose speed and isolation.
How I use this
For any API, I build the pyramid deliberately. A broad base of unit tests for handler logic (fast, precise), a focused layer of integration tests for real database and service wiring, and a small set of functional tests for the critical end-to-end paths. I mock external dependencies so unit tests stay fast and so I can simulate failure modes, but I back those mocks with contract tests so drift gets caught. And before launch, I load-test the hot paths to find the ceiling. The habit is matching the test type to the failure mode I'm guarding against — a unit test won't catch a wiring bug, a functional test won't catch a capacity ceiling, and a load test won't catch a logic error. Each layer earns its place by catching what the others can't.
References
[1] Postman, "What is API Testing?," 2024. [Online]. Available: https://www.postman.com/api-platform/api-testing/
[2] Oyetoke Toby, "How to write unit tests for your REST API," Medium, 2024. [Online]. Available: https://medium.com/@oyetoketoby80/how-to-write-unit-test-for-your-rest-api-f8f71376273f
[3] Merge, "How to run API integration tests," 2024. [Online]. Available: https://www.merge.dev/blog/api-integration-testing
[4] Testsigma, "API Functional Testing – Why Is It Important And How to Test," 2024. [Online]. Available: https://testsigma.com/blog/api-functional-testing/
[5] Grafana, "API Load Testing - Beginners Guide," 2024. [Online]. Available: https://grafana.com/blog/2024/01/30/api-load-testing/
[6] Postman, "What is API mocking?," 2024. [Online]. Available: https://blog.postman.com/what-is-api-mocking/
[7] Testsigma, "Complete Guide to Contract Testing," 2024. [Online]. Available: https://testsigma.com/blog/api-contract-testing/
Knowledge check · Question 1 of 5
A test spins up the full API with a real database and asserts on the HTTP response to `POST /orders`. What layer is this?
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!