All posts

Hands-On Testing · Part 3

API testing explained: what it is, why test the API before the UI, and what to check

ProvaLab team · · 7 min read

In Part 2 our three browser tests took about 11 seconds to run. Most of that time was spent loading pages, rendering them and waiting for a browser, not checking anything. There is a faster place to test most of what matters, one layer down from the screens: the API.

What is an API?

An API (application programming interface) is the set of requests a program can make to another program. In a modern app, every screen talks to the server through an API: when you tap "Place order", the mobile app sends a request like POST /orders with the order details, and the server replies with the result.

Most web and mobile APIs are HTTP APIs, usually following the REST style:

  • A request names a resource with a URL path, such as /orders/42.
  • A method says what to do with it: GET (read), POST (create), PUT or PATCH (update), DELETE.
  • Data travels as JSON, a plain-text format of fields and values.
  • The reply carries a status code: 200 means OK, 201 created, 400 bad request, 401 not signed in, 403 not allowed, 404 not found, 500 the server broke.

Three layers. Top: the mobile app, the website and the restaurant dashboard. Middle: the API, endpoints such as POST /orders, GET /orders/42 and POST /discounts/apply, which every interface calls. Bottom: the services and database. A UI test drives a screen through all three layers in seconds; an API test sends a request straight to the middle layer in milliseconds, with no browser.

The API is where the real logic lives. Whether a customer orders from the mobile app, the website or the restaurant dashboard, the same POST /discounts/apply decides what the discount is. A bug there shows up once, for everyone.

What is API testing?

API testing sends requests directly to the API and checks the responses, without any user interface.

A test is still locate, act, assert, with "locate" replaced by "build the request":

  1. Build a request: method, URL, headers, body.
  2. Send it.
  3. Assert on the response: status, headers, body and timing.

In Python with the requests library it looks like this (we'll run tests like it for real in Part 4):

response = requests.post(
    "https://api.example.com/discounts/apply",
    json={"order_total": 500, "code": "SAVE10"},
    headers={"Authorization": f"Bearer {token}"},
)
assert response.status_code == 200
assert response.json()["total"] == 450

No browser, no page load, no waiting for elements. That test runs in a few milliseconds.

Why test the API before the UI

API tests UI tests
Speed Milliseconds each; thousands in a minute Seconds each
Stability Very stable: no timing, no layout, no cookie banners The flakiest kind of test
When you can start As soon as the endpoint exists, often before the screen does Only once the screen is built
Finds Logic, data, validation, security and error-handling bugs What the user actually sees
Pinpoints the cause Yes: one endpoint, one response Rarely: the failure could be anywhere in the stack
Covers Every client at once (app, website, partners) One screen on one client

The pattern most teams settle on is the testing pyramid applied to the back end: test the business rules thoroughly at the API level, and keep a few UI tests for the journeys users see. The discount logic gets fifty API tests and one UI test, not the other way round.

A second reason that's easy to miss: APIs are products too. If partners or a mobile team build on your API, a change that breaks it breaks them, whether or not your own website still works.

What to check in every response

A request, POST /discounts/apply with order_total 500 and code SAVE10, and its response. Five checks: 1, the status code, 200 here; 2, the headers, right content type and nothing secret leaked; 3, the body shape, the fields and types the contract promises; 4, the values, total 450 and discount 50, the business rule; 5, the timing, 42 ms, within budget.

  1. Status code. 200 for success, the right 4xx for bad input or missing permission, and never a 500. A 500 means the server crashed instead of handling the situation.
  2. Headers. The content type is what the client expects (application/json); caching is right for the data; and nothing sensitive leaks in a header.
  3. Body shape, the contract. The fields the API promises are present, with the right types: total is a number, not the string "450". Clients break on shape changes even when the values are right.
  4. Values, the business rule. 500 with SAVE10 must give total: 450. This is the check that finds the bug that charges every customer the wrong amount.
  5. Timing. The response arrived within the budget, say under 500 ms. One slow endpoint makes every screen that uses it slow.

Kinds of API tests

Functional tests check that each endpoint does its job: create an order, read it back, apply a discount. The bulk of your API suite.

Negative tests send wrong input on purpose, and check that the API refuses it cleanly: a missing field returns 400 with a helpful message, an unknown order returns 404, a discount of 150% is rejected. Many real bugs hide here, as we saw in test design.

Authentication and authorisation tests check that requests without a valid token get 401, and that a signed-in customer gets 403 when asking for someone else's order. The second kind, called broken object-level authorisation, is the most common serious API security bug, and it's easy to test: sign in as customer A, request customer B's order, expect 403.

Contract tests check the shape of requests and responses against an agreed definition, usually an OpenAPI (formerly Swagger) document. When several teams or partners depend on the API, contract tests catch a renamed field before it breaks anyone. Tools such as Pact go a step further, letting each client declare exactly what it needs.

Integration tests through the API check that a sequence works end to end: create an order, apply a code, pay, then read the order back and confirm the total. This is integration testing with the API as the entry point.

Performance tests send many requests at once to find the breaking point, as described in performance testing. Load tests are nearly always API tests.

Test data and authentication

Two practical problems every API tester meets:

  • Tokens. Most APIs need a key or a bearer token. Tests get one in a setup step (sign in with a test account, or read a test key from an environment variable), then reuse it. Never put real credentials in the test code or the repository.
  • Test data. POST /orders creates real rows. Tests should create what they need, use it, and clean up, or run against a test environment that's reset regularly. A test that depends on "order 42 already existing" will fail the day someone deletes order 42.

Tools

Tool Style Good for
Postman / Bruno Point-and-click, with collections you can run from the command line Exploring an API, sharing requests with a team
pytest + requests or httpx Python code Suites that live with the code and run in CI; what we use in Part 4
REST Assured Java Java teams
Karate Its own readable script language Teams that want tests non-developers can read
Playwright's request API JavaScript, Python Mixing API calls into UI tests, such as creating data before a browser test
Schemathesis / Dredd Generate tests from an OpenAPI document Contract checking and fuzzing

If you're starting from nothing, pytest with requests needs the least setup and produces tests that live next to your code.

API testing and the UI tests you already have

The two work together. The fastest reliable UI suites use the API to set the stage: create the test customer and the order through API calls in a few milliseconds, then open the browser only for the part that needs a browser. The sign-up form gets one UI test; the thousand ways an order can be wrong get API tests.

What's next

In Part 4 we build a small API of our own, so there's nothing to sign up for and nothing we don't own, and write a pytest suite against it: functional, negative, authorisation and contract checks, with the real output.

ProvaLab's QA AI Agent runs API test cases on its own and records each request and response against its test case, and AppSec fuzzes your API for the authorisation and input-handling bugs described above. See it in the two-minute product tour.

Prefer to read offline? The whole Hands-On Testing series is one free PDF: get the PDF.

See ProvaLab working on a real app

Your coding agent runs the tests, reads the failures and fixes them.