# API Testing

Testing service interfaces directly — contracts, status codes, payloads, error handling and authorisation.

**Rests on:** Equivalence Partitioning, Test Planning

**Leads to:** Contract Testing, REST and GraphQL

*Software Engineering · Software Testing*


## Why learn this

API testing is the fastest, most reliable way to verify that the services powering modern software — including every AI system you will build — work correctly. When you master this, you unlock: Contract testing (verifying two services agree without running both), Shift-left testing (finding and fixing defects early in the development cycle, when they are cheap to fix), and eventually end-to-end and exploratory testing. AI systems are built from many small services that talk to each other through APIs; if those conversations break, the whole system fails. Testing the API directly catches that breakage before it reaches the user interface.


## The intuition

Think of a restaurant. The kitchen is the server-side logic, the waiter is the API, and the customer is the app or another service. The customer does not walk into the kitchen; they order through the waiter. The waiter takes the order (the request), passes it to the kitchen, and brings back the food (the response). API testing means checking that the waiter does their job correctly: they take the right orders, call the kitchen correctly, and deliver the right food — without waiting for a customer to complain at the table. In software, the 'waiter' is a set of rules called an Application Programming Interface (API): a contract that says what requests you can make and what responses you will receive. Instead of clicking through a user interface to test something, you send a direct request to the API and check the response. This is faster, more stable, and catches problems at the service boundary before they ever reach a human user.


## An analogy

The restaurant waiter is your API. Your test is a demanding customer who orders every item on the menu in every possible way, both normal and strange. The customer sends a request: 'I would like the pasta, please.' The waiter (API) must understand the request, send it to the kitchen (server), and bring back the exact dish. If the waiter mishears, the order goes to the wrong table, or the kitchen sends back a burnt meal, the customer complains. The test checks: Did the waiter speak to the kitchen in the kitchen's language (the right endpoint and method)? Did they deliver the right dish (correct response body)? Did they charge the right price (status code)? Did they refuse to serve someone without a valid reservation (authorisation)?

Now push the analogy to its limit: in a real API, there is no human waiter. The restaurant has a menu (the API documentation) and a set of rules (the contract). The customer might be another service, not a person — so the conversation is machine-to-machine. The waiter never gets tired, but they are also blind: if the kitchen changes a recipe (the server logic changes) without telling the waiter, the waiter will confidently bring the wrong dish. API testing catches that mismatch. The analogy breaks down when you consider that a real restaurant has a human to apologise and reroute; an API just returns an error code. That is exactly why you test — so you catch the error before your users do.


## Definition

API testing is the process of directly verifying that a service's interface — the endpoints (the specific URLs or addresses where the API can be contacted), methods, parameters, request bodies, response codes, and headers — behaves as specified, including handling errors, validating authorisation, and adhering to the contract between the service and its consumers.


## How it connects

This concept builds on your understanding of software testing fundamentals: the idea that the test pyramid favours many fast tests over fewer slow ones. API tests are faster than UI tests because they skip the interface rendering and hit the service logic directly. It is a bridge to Contract testing — which you will learn soon, where two separate services agree on the format of their conversation without needing to run simultaneously — and it complements your existing notes on exploratory testing: API testing often involves sending unexpected requests to discover how the service responds.


## The mathematics

This section of the concept requires no mathematics beyond arithmetic — good news, because you already know everything you need. The 'mathematics' here is not about numbers but about patterns: how a request maps to a response. You will count things (how many status codes a service returns), compare values (does the response contain the expected data?), and check conditions (if the request is unauthorised, is the response code 401?). These are all arithmetic and logic operations you use every day: comparing two numbers, checking if something is present, and following if-then rules. No algebra, no equations. The only counting you will do is tallying how many tests pass and fail, and reviewing the percentages in your test report.

Since this concept involves no algebra, there is no formula. The only 'equation' you need is the assertion: for any response, you check that its status code and body match what the specification says they should be.


## A worked example

```
You send a request to get a user's profile: GET /api/users/42. You expect status code 200 and a body containing the user's name. Your test sends this request and checks the response.

Step 1: Request: GET /api/users/42   # This asks the service for user number 42.
Step 2: Response status: 200   # 200 means 'OK' — the request succeeded.
Step 3: Response body: {'id': 42, 'name': 'Anita', 'role': 'admin'}   # The body contains the data you asked for.
Step 4: Check: status == 200? Yes   # The test passes this assertion.
Step 5: Check: body['name'] == 'Anita'? Yes   # The data matches what you expected.

Result: The test passes. Now change the request to /api/users/999 (a user that does not exist). The response status is 404 (Not Found). Your test should expect 404 and pass even though the request failed — you are testing that the service handles missing resources correctly.
```


## From scratch, in Python

```python
import urllib.request
import json

# Send a GET request to a public test API
url = "https://jsonplaceholder.typicode.com/users/1"
request = urllib.request.Request(url)

# This opens a connection and receives the response
response = urllib.request.urlopen(request)

# Status code: a number like 200 or 404
status = response.status
print("Status:", status)

# Read the body as text
body_text = response.read().decode("utf-8")
print("Body:", body_text)

# Convert text to a Python dictionary (key-value pairs)
data = json.loads(body_text)
print("Name:", data["name"])

# Check that the status is 200 and the name is correct
if status == 200 and data["name"] == "Leanne Graham":
    print("Test passed")
else:
    print("Test failed")
```


## The same thing, with a framework

```python
import requests

url = "https://jsonplaceholder.typicode.com/users/1"
response = requests.get(url)

print("Status:", response.status_code)
print("Body:", response.json())

assert response.status_code == 200
assert response.json()["name"] == "Leanne Graham"
print("Test passed")
```


## What the code does

First, note the Python building blocks you have just met: a variable holds a value, e.g., `url = "..."` stores a string (a piece of text). `import` brings in reusable code. A `dictionary` is a set of key-value pairs, like a phonebook: you look up `data["name"]` to get the name. The `if` statement checks a condition and runs the indented code if it is true. `print()` shows output.

In the plain-Python version, `urllib.request.urlopen(request)` sends the request and returns a response object. `.status` gives the numeric status code. `.read().decode("utf-8")` reads the body as raw bytes and converts them to a readable string. `json.loads` parses that string into a dictionary. Then you manually check the values with an `if`.

The `requests` library is the standard tool developers use for API testing. It does exactly the same thing under the hood: it opens a connection, sends the request, and gives you a response object with `.status_code` and `.json()` — the latter parses the response body for you. The `assert` statement checks a condition and stops the program with an error if it is false. That is the core of API testing: send a request, look at the response, assert what you expect.


## Common misconceptions

**API testing is just calling the endpoint and seeing if it returns 200** — If the API returns a 200 status code, the test passes. That is all I need.

A 200 status only means the server processed the request without throwing an error. The response body could be completely wrong — empty, malformed, or containing data for a different user. A test that only checks status misses all of that. In fact: You must also verify the response body: its structure, its values, and that it matches what the specification promised. Assert on the status code AND on the content.

**Testing the UI is enough; the API will work if the UI works** — If I click through the app and everything appears to work, the API must be fine.

The UI can hide API errors — it might show a generic error message or fall back to cached data that makes the app look functional. Also, the API is often consumed by other services, not just your UI. A broken API can break other apps while your UI still looks fine. In fact: API testing catches issues at the service boundary before any UI hides them. It is faster and more stable than UI testing, because it skips rendering and interaction layers.

**Status codes: 404 means the server is down** — If I see a 404, the server is probably broken or offline.

404 'Not Found' specifically means the requested resource (e.g., /users/999) does not exist. The server is up and correctly handling the request by saying 'there is nothing here.' A server down would give no response at all, or a 502/503. In fact: Each status code has a specific meaning. 404 is a valid response to a bad resource. Your tests should expect and verify the status codes your API is designed to return.

**Error paths are not worth testing if the happy path works** — If my API works for normal requests, that is good enough. Errors are rare.

In production, errors and malicious inputs are common. If you do not test that the API returns a proper 400 for bad input, 401 for missing auth, 404 for missing resources, then those bugs will surface in production where they can cause security issues or take down other services. In fact: Error and authorisation paths deserve as much coverage as success paths. Your test suite should include requests with missing parameters, wrong types, invalid authentication tokens, and nonexistent resources.

**API tests are the same as unit tests** — I already have unit tests; I do not need a separate API test suite.

Unit tests test individual functions in isolation, often with mocked dependencies. They do not verify that the service actually handles an incoming HTTP request correctly, including routing, serialisation, authentication, and error handling. API tests exercise the real integration. In fact: API tests are a distinct level: they test the service's external interface end-to-end, without the UI. They complement unit tests by catching integration breakage.


## In short

API testing means sending direct requests to a service's interface and verifying the response — status code, body, headers — matches the contract. It is faster and more stable than UI testing, catches integration breakage early, and is essential for every AI system built from small services. Remember: test the happy path, but also test errors and authorisation; a 200 is not enough without checking the content; and use tools like `requests` to automate and assert on responses.


## Where this is used

API testing is everywhere in modern AI systems, and here is how.

**Large language models (LLMs).** An LLM like ChatGPT is not a single program; it is a service that listens on an API. When you talk to a chatbot, your app sends a request to the LLM's API with your prompt and parameters (like temperature). API testing verifies that the service responds within a time limit, returns valid JSON (even when the model produces incomplete text), and handles errors when the prompt is too long or the model hits a rate limit. Developers write tests that send a prompt and assert that the response contains `choices`, a standard structure. They also test that the API rejects requests without a valid API key (returns 401).

**Small models on devices.** When a model runs on a phone or a laptop, the device itself is the consumer of a cloud API for model updates or federated learning. API testing checks that the device can sync model weights (a large JSON payload) without corruption, and that retries happen cleanly when the network disconnects. The tests verify the API's handling of partial uploads and its status codes for interrupted requests.

**Retrieval-augmented generation (RAG).** RAG systems retrieve documents from a vector database and feed them to an LLM. The retrieval service is an API. Testing it means: send a query, check that the response returns the correct top-k documents, that the embedding of the query is in the right format, and that the service returns 200 even when there are zero matching documents. You also test that the retrieval API rejects a malformed vector (e.g., wrong dimensions) with a 400 Bad Request.

**Agentic systems.** An agent that plans and calls tools is basically a loop: it sends requests to multiple APIs (search, calendar, code execution). API testing is critical here because one broken tool call can make the whole agent fail. You test that each tool API returns the expected response schema, that error responses are propagated correctly, and that timeouts happen within the agent's planning horizon. You write tests that simulate an agent calling a tool and asserting the tool's output is structured as the agent expects.

**Everyday applied work — recommendation, vision, fraud detection, speech.** These are all services exposed via APIs. Recommendation systems: test that the `/recommend` endpoint returns a list with the required fields and handles a user with no history. Vision systems: test that `/detect` returns bounding boxes in the right format and rejects invalid images. Fraud detection: test that `/score` returns a number between 0 and 1 and that a missing feature value returns a 400. Speech: test that `/transcribe` handles audio files of different lengths and formats, and that a corrupted file returns an error code.

**What a developer writes.** They use Python's `requests` library (or a testing framework like `pytest` with `requests`). They write functions that send requests, then assert expected status codes and body fields. They organise these into test files that run in CI (continuous integration) on every commit. What breaks when it is done badly: tests that only check the happy path miss error and authorisation failures; tests that are too slow or flaky (e.g., depending on network state) get ignored by the team; forgetting to test payload schema means a change in the API breaks every consumer silently until production. A well-built API test suite catches integration breakage before release — this is the essence of the 'contract testing' you will learn next.


---

Read this on the web: https://basavapurushottam.com/learn-ai/api-testing
