API Testing Strategies: What to Test, and How Often

Most API test suites are slow, flaky, and still miss the outages. Usually because every layer is being asked to answer a question it cannot answer.

Updated 25 August 2026 · 13 min read

A working API testing strategy assigns each question to the cheapest layer that can answer it: unit tests for logic, integration tests for wiring, contract tests for breaking changes, end-to-end tests for whole journeys, and production monitoring for everything that only exists in production. The common failure is pushing questions upward, using slow end-to-end tests to catch problems a contract test would have caught in seconds.

Key takeaways

  • Assign every check to the cheapest layer that can answer it. Cost rises and reliability falls at every step up the stack.
  • Contract tests are the highest-value layer most teams skip. They catch the breakage that actually causes incidents: a producer changing a field a consumer relies on.
  • A 200 status code is not a passing test. Assert on the response body, or you are testing that a server exists.
  • No amount of pre-deploy testing tells you about expired certificates, rotated credentials, third-party outages or DNS. Those only exist in production.
  • Test count is a vanity metric. Track how long the suite takes, how often it fails for reasons unrelated to the change, and what fraction of incidents it caught.

The five layers, and the question each one answers

Almost every problem with an API test suite comes from one mistake: asking a layer to answer a question it is structurally unable to answer. An end-to-end test cannot tell you why a function returns the wrong value. A unit test cannot tell you that a consumer depends on a field you just renamed. Neither can tell you that the certificate expires on Sunday. Getting the strategy right is mostly a matter of routing each question to the layer that can answer it cheaply and reliably.

LayerThe question it answersWhat it cannot see
UnitDoes this function do the right thing with these inputs?Anything that crosses a process or network boundary.
IntegrationDo these components work when actually wired together?Whether the consumers of this API agree with its shape.
ContractHave I broken anyone who depends on me?Whether the deployed system actually works at runtime.
End-to-endDoes a complete user journey work through the real stack?Production data, production scale, production dependencies.
Production monitoringIs the live API working right now, for real callers?Logic bugs on paths no check exercises.

The layers form a cost gradient. A unit test runs in a millisecond and either passes or fails for exactly one reason. A production check runs against a system with real load, real data, and real third parties, so when it fails the cause could be anything. Push a question up the stack and you pay for it twice: once in the time the test takes, and again every time it fails for a reason that has nothing to do with your change.

The rule that resolves most arguments about test placement: put a check at the lowest layer that can actually see the failure. If a contract test can catch it, a nightly end-to-end run is the wrong place for it.

What to test, by category

Coverage of an API is not one thing. These are the seven categories worth deliberate attention, with the layer each belongs to. Most suites over-invest in the first row and skip the rest entirely.

CategoryWhat to checkWhere it belongs
FunctionalStatus codes, response shape, field types, required fields present, values correct for known inputs.Unit and integration
ContractThe response still satisfies the schema every consumer was written against. No field removed, renamed, retyped, or made nullable.Contract, on both sides
Edge and dataEmpty results, nulls in optional fields, pagination boundaries, unicode and emoji, very large payloads, duplicate submissions.Integration
Error handlingA malformed body returns 400 and not 500. Unknown ids return 404. The error body has a stable, documented shape.Integration
Authentication and authorisationNo token is rejected. An expired token is rejected. A valid token for the wrong tenant cannot read another tenant's data.Integration
PerformanceResponse time against a stated budget, at a realistic concurrency, with a realistic data volume.Load, plus production
SecurityInjection through every parameter, object-level authorisation on every id, and no secrets or internal detail leaking in error responses.Integration and dedicated review

Two of these deserve emphasis because they are the ones that produce real incidents rather than red builds.

The authorisation row is the one that gets skipped and the one that hurts most. Testing that a request without a token is rejected is easy and nearly worthless, because that path is almost never the bug. The bug is that a perfectly valid token belonging to customer A can read customer B's record by changing an id in the URL. That test has to be written deliberately, with two real identities, and no framework writes it for you.

The error handling row matters because error paths are where APIs are least exercised and most inconsistent. An endpoint that returns a clean JSON error object for a missing field and a raw stack trace for a malformed one has two different contracts, and every consumer will discover the second one in production.

How often each layer should run

Frequency is half the strategy and the half that usually goes unstated. A test that runs at the wrong cadence is either slowing down every commit or discovering last Tuesday's breakage.

LayerRuns whereHow oftenBudget
UnitDeveloper machine and CIEvery commitSeconds. If it is minutes, something is talking to a network.
IntegrationCI, against ephemeral dependenciesEvery pull requestUnder 10 minutes, or people stop waiting for it.
ContractCI, on the producer and every consumerEvery pull request on both sidesSeconds. It is schema comparison, not execution.
End-to-endCI against a deployed environmentPer release, plus nightlyTens of minutes. Keep the set small and high-value.
LoadA dedicated environmentBefore launches and capacity changes, then quarterlyHours, scheduled deliberately.
Production monitoringAgainst production, from outside itContinuously, every minute or few minutes, foreverAlways on. This is the only layer that runs when nobody is deploying.

The bottom row is the one that changes character rather than degree. Every layer above it runs because somebody made a change. The production layer runs because time passed, and time is what breaks certificates, credentials, quotas, disks and third parties.

Contract testing, the layer most teams skip

If you add one thing to an existing suite, add this. Contract testing is the highest ratio of incidents prevented to effort spent, and it is missing from most strategies because it is the only layer that requires two teams to agree on something.

The failure it prevents is specific. A producer team renames a field, makes an optional field required, changes a number to a string, or starts returning null where it never did. Every test on the producer side passes, because the producer's own tests were updated in the same commit. The consumer breaks in production, often silently, and often not immediately.

A contract test inverts the direction of the check. Instead of the producer asserting what it returns, each consumer declares what it needs, and the producer's build fails if it stops satisfying any declared expectation. The consumer's build fails if it starts depending on something the producer never promised. Neither side can break the other without a red build first.

# Before: the producer returns
{ "id": 42, "customer_name": "Acme", "seats": 12 }

# After a "harmless" cleanup, still 200, still valid JSON:
{ "id": 42, "name": "Acme", "seats": "12" }
#                    ^ renamed              ^ now a string

# Producer's own tests: PASS. They were updated in the same commit.
# Consumer in production: reads customer_name -> undefined
#                         parses seats -> "12" * 2 = "1212"
#
# A consumer-declared contract fails the producer's build instead,
# because a consumer registered an expectation on customer_name:number.

Two practical notes. Contract tests are not schema validation of your own responses against your own schema, which proves only that you agree with yourself. The value comes from the expectations being declared by the consumers. And a published OpenAPI document is not a contract test either, it is documentation, until something in CI fails when the implementation and the document disagree.

A useful question for any API team: if someone renamed a response field this afternoon, what would fail, and would it fail before or after the deploy? If the honest answer is "a customer would tell us", contract testing is the gap.

Where testing stops and monitoring takes over

This is the boundary the strategy is really about, and it is drawn in the wrong place more often than any other. Tests answer a question about a change: is this new code correct? Monitoring answers a question about a moment: is the live system working now? Neither substitutes for the other, and a suite at 100% coverage tells you nothing about whether the API is up.

What follows is the honest list of things that pass every test in CI and still take an API down, because none of them exist in the environment the tests ran against:

  • The TLS certificate expired. Staging has a different one, and probably a longer-lived one.
  • A credential rotated. The API key in CI is not the API key in production.
  • A third party you call is degraded. Your tests mocked it, correctly, which is exactly why they cannot see this.
  • DNS changed, or a record expired, or propagation went wrong for a subset of resolvers.
  • A quota or rate limit was reached, on your side or your provider's.
  • A WAF or CDN rule started blocking a legitimate request pattern that no test sends.
  • Real production data hit a path no fixture covers: a null in a field that is never null in seed data, or a record large enough to time out.
  • Concurrency. The code is correct in isolation and races under production load.

Every item on that list is invisible to a test suite by construction, not by oversight. That is the argument for the production layer: not that the tests were inadequate, but that they were answering a different question.

The relationship is the same one that separates a test run from synthetic monitoring, and the same reason a passing suite is compatible with service degradation that no alert catches. Correct and available are different properties.

Designing the production check

A production API check is not a test moved to production. It answers a narrower question and it has a constraint tests do not: it runs forever, so it must be cheap, safe to repeat, and quiet unless something is genuinely wrong.

Pick an endpoint that proves something

A /health endpoint returning {"status":"ok"} is the most commonly monitored and least informative choice available, because most of them only prove the process is running. If it does not touch the database, the cache and the critical dependency, it will report healthy through an outage. Either make the health endpoint check its dependencies, or point the monitor at a real read endpoint that exercises the stack.

Assert on the body, not just the status

This is the single most important setting and the one most often left at its default. A 200 proves a response was produced. It does not prove the response was correct, and plenty of applications catch their own exceptions, render an apology, and return it with a 200. Check for a string that must be present, and for strings that must never appear.

# Weak: passes while the API returns an empty list forever
curl -sf https://api.example.com/v1/health

# Better: authenticated, hits real data, asserts on the payload
curl -sS -m 10 \
  -H "Authorization: Bearer $MONITOR_TOKEN" \
  https://api.example.com/v1/plans \
  | tee /tmp/r.json \
  | grep -q '"currency"' || exit 1

# And the negative assertion, which catches the 200-with-an-error case
grep -qiE 'something went wrong|temporarily unavailable|<html' /tmp/r.json && exit 1

exit 0

Make it safe to run every minute, forever

Read-only endpoints are the obvious choice. Where you must exercise a write path, use a dedicated account whose records are disposable, and make the operation idempotent so a thousand runs a day do not accumulate. Give the monitor its own credential too, so that revoking it never touches a real customer and so its traffic is identifiable in your logs and excluded from analytics.

Check from more than one place, and confirm before paging

A single-location check cannot distinguish "the API is down" from "the path between this one probe and the API is down", and it will page somebody for the second one. Running the same check from several regions turns a false alarm into a diagnosis: one location failing while five others pass is a network or CDN problem, not an outage. Requiring confirmation from a second region, and more than one consecutive failure, removes most of the noise an on-call rotation would otherwise absorb.

This is the shape of API monitoring in practice: request the endpoint with the headers it expects, including a bearer token, assert on the response body rather than the status alone, record the response time against a baseline, and alert only on confirmed failures. Worth being clear about the limit: a content check is not schema validation. It catches an empty or error-shaped payload, not a field quietly changing type. That one belongs in the contract layer, which is where this article started.

Five things that make an API suite worthless

  1. Asserting only on status codes. A suite that checks for 200 and stops is testing that a web server exists. Assert on the body.
  2. Mocking the thing under test. Mock what you do not own. Mocking your own database or your own service means the test proves the mock behaves as configured.
  3. Tolerating flakes. One test that fails 5% of the time trains everyone to re-run the build, and the day it catches a real bug nobody believes it. Fix it or delete it.
  4. Fixtures that look nothing like production. Seed data with three tidy rows and no nulls will never reproduce the failure a real dataset causes. Where privacy allows, derive fixtures from anonymised production shapes.
  5. Counting tests. Coverage percentage and test count are vanity metrics. Track suite duration, flake rate, and the share of incidents the suite caught before release.

That last measurement is uncomfortable and worth doing anyway. Go through the last ten incidents and mark, for each, which layer could have caught it and which layer did. The answer usually reallocates effort immediately, and in most teams it points at the two layers this article spends the most time on: contracts before the deploy, and monitoring after it.

A strategy is only real if it says what you are not testing. Write that part down too, because the untested surface is where the next incident is.

See how Pulsetic's API monitoring catches this from the outside, across 15+ locations.

Frequently asked questions

  • What are the main types of API testing?

    Functional testing checks that endpoints return the right status codes and payloads. Integration testing checks that components work when wired together. Contract testing checks that a change has not broken any consumer. End-to-end testing checks a complete journey through the real stack. Load testing checks behaviour at realistic concurrency. Security testing covers injection, object-level authorisation and data leakage in error responses. Production monitoring covers the live system continuously. Each answers a different question, and the strategy is deciding which layer answers which.

  • How often should API tests run?

    Unit tests on every commit, in seconds. Integration and contract tests on every pull request, with integration kept under about ten minutes so people still wait for it. End-to-end tests per release and nightly. Load tests before launches and capacity changes, then quarterly. Production monitoring runs continuously, every minute or few minutes, because it is the only layer that runs when nobody is deploying, and time is what expires certificates and credentials.

  • What is the difference between API testing and API monitoring?

    Testing answers a question about a change: is this new code correct? It runs in CI, against staging, on a schedule you control, with mocked dependencies. Monitoring answers a question about a moment: is the live API working right now? It runs against production, continuously, with real data, real credentials and real third parties. A suite at full coverage tells you nothing about whether the API is up, and a passing monitor tells you nothing about whether a code path is correct.

  • What is contract testing and why does it matter?

    Contract testing has each consumer declare the fields and types it depends on, then fails the producer's build if a change stops satisfying any of those expectations. It matters because it catches the most common cause of API incidents: a producer renaming a field, changing its type, or making it nullable, with every producer-side test passing because they were updated in the same commit. Validating your own responses against your own schema does not achieve this, since it only proves you agree with yourself.

  • Is a 200 status code enough to consider an API test passed?

    No. A 200 proves a response was produced, not that it was correct. Applications routinely catch their own exceptions, render an error message and return it with a 200, and an endpoint returning an empty list forever will pass a status-code check indefinitely. Assert that expected fields are present with the right types, and add negative assertions for error text that should never appear in a healthy response.

  • What should an API monitor check in production?

    Point it at an endpoint that exercises the real stack rather than a health route that only proves the process is running. Send the headers the endpoint expects, including an API key or bearer token, using a dedicated monitoring credential. Assert on the response body, not just the status. Record response time against a baseline so slow degradation is visible before failure. Run the check from several regions and require confirmation from a second one before alerting, which distinguishes a real outage from a network path problem.