testingdevops

Five Kinds of API Test and When Each One Earns Its Place

Two engineers can both say the API is tested, both be telling the truth, and still disagree about whether it is ready. Functional, integration, regression, load and security testing, what each one catches, what each one structurally misses, how to spread your effort between them, and the one minute authorisation check that finds the most common serious API vulnerability.

Tauseef Fayyaz

Tauseef Fayyaz

Feb 6, 20265 min read0 views

Testing an API is not one activity

People say they have tested the API and mean any of five different things, which is why two engineers can both be telling the truth and still disagree about whether something is ready.

Each kind answers a different question, costs a different amount, and misses a different set of problems. Knowing which one you have run, and therefore which problems could still be waiting, is most of the skill.

Functional tests: does this endpoint do what it says

Send a request, check the response. Correct status code, correct body shape, correct values. Then do it again with bad input and check you get a sensible error rather than a stack trace.

What it catches: the endpoint returning the wrong thing, missing validation, error cases nobody implemented.

What it misses: anything involving another system. Every dependency here is a mock, and a mock is a statement about how you believe the other system behaves, which may not be true.

Where to spend your effort: the unhappy paths. Everybody tests that a valid request works. Far fewer test what happens with a missing required field, a string where a number belongs, or an identifier that does not exist. That is where the bugs are.

Integration tests: does it work with the real things attached

Same requests, but now against a real database, a real cache, a real message queue.

What it catches: the large category of problems that only exist between components. A migration that never ran. A transaction that does not roll back the way you assumed. A query that is fine against ten rows and unusable against a million. Serialisation differences that mocks hid.

What it misses: load related behaviour, and anything involving a third party you are still not really calling.

The trade off: slower and more annoying to set up. Containers make this a lot less painful than it used to be, and it is worth the setup cost, because this layer catches the failures that embarrass you in production.

Keep far fewer of these than functional tests. They are for the interactions, not for re-checking business logic you already covered.

Regression tests: did we break something that used to work

Not a separate style of test so much as a discipline: keep the tests, run them all on every change.

What it catches: the most common failure in a maturing codebase, which is a fix over here quietly breaking something over there.

What it costs: the suite grows and grows. Eventually it takes long enough that people stop running it locally, and at that point it has stopped protecting you.

How to keep it honest: when you fix a bug, add the test that would have caught it, every time. That single habit builds a suite that reflects your actual failure history rather than what somebody imagined in advance. And delete tests that no longer test anything real, because a suite nobody trusts gets skipped wholesale.

Load tests: does it hold up under traffic

Send realistic traffic at a realistic rate and watch what happens to response times, error rates and resource use.

What it catches: the connection pool that is too small, the query with no index that nobody noticed at low volume, the memory that grows and never comes back, the timeout that turns a slow dependency into a total outage.

What it misses: almost everything, if the traffic is not realistic. This is the trap. Hitting one endpoint with identical requests warms every cache and tells you nothing. Real traffic is mixed, uneven, spiky, and full of cache misses.

The number that matters: not the average. Average response time hides the problem. Look at the 95th and 99th percentile, because that is the experience of the users who are having a bad time, and those are the ones who leave.

Security tests: can this be misused

Authentication actually enforced. Authorisation checked on every route rather than most of them. Input handled safely. Rate limits present. Secrets not appearing in logs or error responses.

What it catches: the failure mode where the feature works perfectly and that is the problem, because it works perfectly for someone who should not have access.

What it misses: it cannot prove there is nothing there. A clean scan means the known patterns are absent, not that the system is safe.

The one to check first: object level authorisation. Take a valid request from user A, change the identifier in the path to something belonging to user B, and send it. If it returns B's data, you have found the single most common serious API vulnerability, and you found it in about a minute.

How to spread your effort

Rough shape that works for most teams:

Many functional tests. Fast, cheap, run constantly, cover the unhappy paths thoroughly.

Some integration tests. Cover the interactions between components, not the logic again.

A few load tests. Run before a major release and whenever you change something that touches data volume.

Security checks automated in the pipeline, plus the manual authorisation check above on anything new that returns user data.

All of it kept and re-run on every change, which is the regression part.

The mistake to avoid is spending three weeks writing exhaustive tests against mocks, achieving a coverage number that looks excellent, and shipping something that has never once talked to the real database. Coverage measures which lines ran. It does not measure whether you tested anything that matters.

The habit worth building

Before you call something tested, ask which of the five you actually ran, and then say out loud what class of problem is still possible.

"I have functional tests passing, so the logic is right, but this has never been run against real data volumes" is a genuinely useful sentence. It is honest, it tells your team where the risk is, and it is a great deal better than "yes, it is tested".

testingdevops

Tauseef Fayyaz

Written by Tauseef Fayyaz

Lead Full Stack Engineer & Career Mentor. I lead an engineering team by day and mentor engineers through job hunts, promotions and career switches the rest of the time.


Comments (0)

Comments are closed for now.

No comments yet.

Work with me

Stuck on something specific?

Writing only gets you so far. If you want an answer to your situation rather than the general case, book a session and we will work through it together. Every session is free; a few slots open each week.

Follow along

New writing, resources and project ideas land here first.