API Testing Examples: 22 Real Test Cases With Code
Quick answer
An API testing example is one request with a known input, an expected result and an assertion that proves it. Good suites mix functional CRUD checks, negative and validation cases, auth, schema, pagination, rate limit, idempotency, a performance smoke test and basic security probes. Below are 22 examples with REST Assured, pytest and Postman assertions.
Reviewed by Rishi Gaurav
Part of the API Testing hub. Start with the pillar guide: What is API testing? Types, tools and how it works.
These API testing examples are the ones we reach for when someone asks "what does a good API test actually look like?" Each example is a single request, the result you should expect, and an assertion that fails loudly when the API misbehaves. If you need the theory first (what API testing is, where it sits in the pyramid, how to plan a suite), start with the complete guide to API testing and come back here for the concrete cases.
There are 22 examples, grouped by type: functional, negative and validation, authentication and authorization, schema and contract, pagination, rate limiting, idempotency, a performance smoke test and security basics. Assertions rotate between three styles so you can copy the one that matches your stack: REST Assured (Java), pytest + requests (Python) and Postman test scripts (JavaScript).
How to read these API testing examples
Every example uses the same sample API: an orders service at https://api.example.test/v1 with /orders, /customers and /auth/token endpoints, bearer-token auth and an OpenAPI 3.1 spec. Swap in your own base URL and paths; the patterns carry over unchanged.
Each example has four parts:
- Request: method, path, headers and body.
- Expected result: status code, key body fields and headers.
- Why it matters: the bug it catches.
- Assertion: a runnable snippet in one of the three styles.
The shared setup for the Python and Java snippets looks like this:
# settings.py: imported by every test module (from settings import BASE, VALID_ORDER)
import os
BASE = os.getenv("API_BASE", "https://api.example.test/v1")
VALID_ORDER = {"customerId": "c_100",
"items": [{"sku": "BOOK-1", "quantity": 2, "unitPrice": 12.50}]}
# conftest.py
import json, os, pytest, requests
from settings import BASE, VALID_ORDER
def _token(client_id, client_secret):
r = requests.post(f"{BASE}/auth/token", json={
"client_id": client_id, "client_secret": client_secret}, timeout=10)
r.raise_for_status()
return {"Authorization": f"Bearer {r.json()['access_token']}"}
@pytest.fixture
def auth():
return _token(os.environ["CLIENT_ID"], os.environ["CLIENT_SECRET"])
@pytest.fixture
def auth_user_a():
return _token(os.environ["USER_A_ID"], os.environ["USER_A_SECRET"])
@pytest.fixture
def auth_user_b():
return _token(os.environ["USER_B_ID"], os.environ["USER_B_SECRET"])
@pytest.fixture
def created_order(auth):
r = requests.post(f"{BASE}/orders", json=VALID_ORDER, headers=auth, timeout=10)
assert r.status_code == 201
order = r.json()
yield order
requests.delete(f"{BASE}/orders/{order['id']}", headers=auth, timeout=10) # 404 is fine
@pytest.fixture(scope="session")
def order_schema():
with open("schemas/order.schema.json") as f: # exported from components.schemas.Order
return json.load(f)
// BaseTest.java: test classes extend this and static-import RestAssured and Hamcrest matchers
static RequestSpecification authed;
static final String VALID_ORDER =
"{\"customerId\":\"c_100\",\"items\":[{\"sku\":\"BOOK-1\",\"quantity\":2,\"unitPrice\":12.50}]}";
@BeforeAll
static void setup() {
RestAssured.baseURI = System.getenv().getOrDefault("API_BASE", "https://api.example.test/v1");
authed = new RequestSpecBuilder()
.addHeader("Authorization", "Bearer " + TokenHelper.fetch()) // your client-credentials call
.setContentType(ContentType.JSON)
.build();
}
Here are all 22 at a glance:
| # | Example | Type | Request | Expected |
|---|---|---|---|---|
| 1 | Health check | Functional | GET /health | 200, status: "ok" |
| 2 | Create an order | Functional | POST /orders | 201, Location header, id |
| 3 | Read it back | Functional | GET /orders/{id} | 200, same fields as created |
| 4 | Update status | Functional | PATCH /orders/{id} | 200, new status persisted |
| 5 | Delete then 404 | Functional | DELETE then GET | 204, then 404 |
| 6 | Missing required field | Negative | POST /orders without items | 400, field-level error |
| 7 | Wrong data type | Negative | quantity: "two" | 400 or 422 |
| 8 | Boundary values | Negative | quantity: 0, 1, 1000, 1001 | reject 0 and 1001 |
| 9 | Unknown resource | Negative | GET /orders/does-not-exist | 404, error body |
| 10 | No token | Auth | any call, no header | 401, WWW-Authenticate |
| 11 | Expired token | Auth | expired JWT | 401, not 500 |
| 12 | Another user's order (BOLA) | Auth | user B reads user A's order | 403 or 404 |
| 13 | Response matches schema | Schema | GET /orders/{id} | body valid against spec |
| 14 | Content type and headers | Contract | any GET | application/json, no stack info |
| 15 | First and next page | Pagination | GET /orders?limit=20 | 20 items, next link |
| 16 | Last page and bad cursor | Pagination | last page, junk cursor | no next; 400 on junk |
| 17 | Rate limit | Rate limit | burst past the limit | 429, Retry-After |
| 18 | Idempotent retry | Idempotency | same Idempotency-Key twice | one order, same response |
| 19 | PUT is idempotent | Idempotency | same PUT twice | identical state |
| 20 | Latency smoke | Performance | GET /orders x 20 | p95 under budget |
| 21 | SQL injection probe | Security | ?status=' OR '1'='1 | 400, no data leak |
| 22 | Mass assignment | Security | POST with "role":"admin" | field ignored or rejected |
Functional API testing examples (1–5)
Functional examples prove the endpoint does its job on valid input. They are the backbone of API functional testing and should run on every commit.
Example 1 — Health check. GET /health returns 200 with {"status":"ok"} in under a second. It catches a broken deploy before the rest of the suite wastes time on it.
def test_health():
r = requests.get(f"{BASE}/health", timeout=5)
assert r.status_code == 200
assert r.json()["status"] == "ok"
assert r.elapsed.total_seconds() < 1
Example 2 — Create an order. POST /orders with one line item returns 201, a Location header and a body whose total equals price times quantity. It catches wrong status codes (200 instead of 201) and arithmetic bugs.
@Test
void createOrderReturns201() {
given().spec(authed)
.body("""
{"customerId":"c_100","items":[{"sku":"BOOK-1","quantity":2,"unitPrice":12.50}]}
""")
.when()
.post("/orders")
.then()
.statusCode(201)
.header("Location", matchesPattern(".*/orders/[a-z0-9_]+$"))
.body("id", notNullValue())
.body("status", equalTo("PENDING"))
.body("total", equalTo(25.0f));
}
Example 3 — Read it back. GET /orders/{id} on the id from example 2 returns 200 and the same customerId, items and total. Round-trip checks catch fields that are accepted on write but never stored.
Generate API test cases from your spec (free)
Free, runs in your browser, no sign-up needed.
Example 4 — Update status. PATCH /orders/{id} with {"status":"PAID"} returns 200, and a fresh GET shows PAID. Always re-read after a write; some APIs echo the request body without saving it.
Example 5 — Delete, then 404. DELETE /orders/{id} returns 204, and the next GET returns 404. A 200 on the second call means a soft delete is leaking.
def test_delete_then_404(auth, created_order):
oid = created_order["id"]
assert requests.delete(f"{BASE}/orders/{oid}", headers=auth, timeout=10).status_code == 204
assert requests.get(f"{BASE}/orders/{oid}", headers=auth, timeout=10).status_code == 404
Writing these by hand for every endpoint is slow. If you have an OpenAPI spec, paste it into the free API test case generator to get a starter pack of functional, negative and auth cases for each operation, then adapt the assertions above.
Negative and validation API test examples (6–9)
Negative tests check that bad input gets a clean, documented rejection: a 4xx with a useful error body, never a 500 and never a silent success. For a deeper catalogue, see how to write API test cases.
Example 6 — Missing required field. POST /orders without items returns 400 and an error that names the field.
// Postman test script
pm.test("400 for missing items", () => {
pm.response.to.have.status(400);
const err = pm.response.json();
pm.expect(err.errors).to.be.an("array").that.is.not.empty;
pm.expect(err.errors[0].field).to.eql("items");
});
Example 7 — Wrong data type. "quantity": "two" returns 400 or 422 (whichever your spec documents). A 500 here means the type check happens after the value reaches the database.
Example 8 — Boundary values. If the spec says quantity is 1–1000, test 0, 1, 1000 and 1001. Off-by-one errors live at the edges, so parametrize instead of copying tests:
@pytest.mark.parametrize("qty,expected", [(0, 400), (1, 201), (1000, 201), (1001, 400)])
def test_quantity_bounds(auth, qty, expected):
body = {"customerId": "c_100", "items": [{"sku": "BOOK-1", "quantity": qty, "unitPrice": 1}]}
r = requests.post(f"{BASE}/orders", json=body, headers=auth, timeout=10)
assert r.status_code == expected
Example 9 — Unknown resource. GET /orders/does-not-exist returns 404 with the standard error shape (for example RFC 9457 problem details), not an empty 200.
Authentication and authorization test examples (10–12)
Authentication proves who is calling; authorization decides what they may touch. Broken object-level authorization (BOLA) has been number one on the OWASP API Security Top 10 since the list began, so example 12 is not optional. Login endpoints have their own pack: see API test cases for login.
Example 10 — No token. Any protected call without Authorization returns 401 with a WWW-Authenticate header, as RFC 6750 describes for bearer tokens.
@Test
void noTokenReturns401() {
given() // no auth spec on purpose
.when().get("/orders")
.then().statusCode(401)
.header("WWW-Authenticate", containsString("Bearer"));
}
Example 11 — Expired token. Replay a token past its exp. Expect 401. A 500 usually means the JWT library throws and nothing catches it.
Example 12 — Another user's order (BOLA). User A creates an order; user B requests it by id. Expect 403 or 404, and make sure the body contains none of A's data.
def test_bola(auth_user_a, auth_user_b):
oid = requests.post(f"{BASE}/orders", json=VALID_ORDER, headers=auth_user_a, timeout=10).json()["id"]
r = requests.get(f"{BASE}/orders/{oid}", headers=auth_user_b, timeout=10)
assert r.status_code in (403, 404)
assert "customerId" not in r.text
Schema, contract and pagination examples (13–16)
These examples check the shape of responses and the mechanics of list endpoints. They catch drift, where code changes but the spec does not.
Example 13 — Response matches the schema. Validate the body of GET /orders/{id} against the Order schema from your spec. In Python, jsonschema does it in two lines:
from jsonschema import validate
def test_order_schema(auth, created_order, order_schema):
r = requests.get(f"{BASE}/orders/{created_order['id']}", headers=auth, timeout=10)
validate(instance=r.json(), schema=order_schema) # raises ValidationError on drift
In REST Assured, use the json-schema-validator module: .body(matchesJsonSchemaInClasspath("order.schema.json")). In Postman, pm.response.to.have.jsonSchema(schema).
Example 14 — Content type and safe headers. Every JSON response has Content-Type: application/json, and error responses leak no stack traces or server versions.
pm.test("JSON content type, no stack traces", () => {
pm.expect(pm.response.headers.get("Content-Type")).to.include("application/json");
pm.expect(pm.response.text()).to.not.match(/at [\w.]+\(.*:\d+\)|Traceback/);
});
Example 15 — First and next page. GET /orders?limit=20 returns exactly 20 items and a next cursor or link. Following next returns the following 20 with no duplicates.
def test_pagination_no_duplicates(auth):
first = requests.get(f"{BASE}/orders", params={"limit": 20}, headers=auth, timeout=10).json()
second = requests.get(first["next"], headers=auth, timeout=10).json()
ids_1 = {o["id"] for o in first["data"]}
ids_2 = {o["id"] for o in second["data"]}
assert len(first["data"]) == 20
assert ids_1.isdisjoint(ids_2)
Example 16 — Last page and bad cursor. The final page has no next. A tampered cursor returns 400, not page one. limit=0, limit=-1 and limit=10000 should be rejected or capped as documented. The full set of edge cases is in pagination test cases.
Rate limit, idempotency, performance and security examples (17–22)
Free ZIP: 5 CSV packs + guide
API Test Case Library
159 ready-made API test cases in five CSV packs: login, pagination, search and filter, auth (API key, JWT, OAuth 2.0) and errors. Copy, adapt, run.
Download FreeThese are the examples most suites skip, and the ones behind most production incidents.
Example 17 — Rate limit. Send requests past the documented limit. Expect HTTP 429 and a Retry-After header. Run this against a test environment only.
def test_rate_limit(auth):
codes = [requests.get(f"{BASE}/orders", headers=auth, timeout=10) for _ in range(120)]
limited = [r for r in codes if r.status_code == 429]
assert limited, "no 429 after 120 calls"
assert "Retry-After" in limited[0].headers
Example 18 — Idempotent retry. Send the same POST /orders twice with the same Idempotency-Key. Expect one order, and the same id in both responses. This is what protects customers from double charges when a mobile client retries on a timeout.
@Test
void sameIdempotencyKeyCreatesOneOrder() {
String key = UUID.randomUUID().toString();
String id1 = given().spec(authed).header("Idempotency-Key", key).body(VALID_ORDER)
.post("/orders").then().statusCode(201).extract().path("id");
String id2 = given().spec(authed).header("Idempotency-Key", key).body(VALID_ORDER)
.post("/orders").then().statusCode(anyOf(is(200), is(201))).extract().path("id");
assertEquals(id1, id2);
}
Example 19 — PUT is idempotent. RFC 9110 defines PUT and DELETE as idempotent. Send the same PUT /customers/{id} twice; the second GET must match the first exactly, including updatedAt if your contract says it only changes on a real update.
Example 20 — Latency smoke. This is not a load test. It is a cheap gate that catches an accidental N+1 query. Call GET /orders 20 times and check the 95th percentile against a budget.
import statistics
def test_latency_smoke(auth):
times = [requests.get(f"{BASE}/orders", headers=auth, timeout=10).elapsed.total_seconds()
for _ in range(20)]
p95 = statistics.quantiles(times, n=20)[18]
assert p95 < 0.5, f"p95 {p95:.3f}s over 500 ms budget"
For real load, move to a dedicated tool; API load testing tools compares them.
Example 21 — SQL injection probe. GET /orders?status=' OR '1'='1 returns 400 or an empty list for a status that doesn't exist, never every order in the table and never a database error.
pm.test("injection string is rejected or harmless", () => {
pm.expect(pm.response.code).to.be.oneOf([200, 400]);
pm.expect(pm.response.text()).to.not.match(/SQL|syntax error|ORA-\d+/i);
if (pm.response.code === 200) pm.expect(pm.response.json().data).to.have.lengthOf(0);
});
Example 22 — Mass assignment. POST /customers with an extra "role":"admin" or "credit": 99999 must ignore or reject those fields. Re-read the record and check they did not stick. This maps to OWASP API3:2023, broken object property level authorization.
From examples to a suite you can run in CI
Examples become useful when they run on every pull request. Three habits make that work:
- One file per resource, one test per behaviour.
test_orders.pyholds examples 2–9;test_orders_auth.pyholds 10–12. A failing test name should tell you the bug. - Create your own data. Each test creates the order it needs and deletes it after, so tests can run in any order and in parallel.
- Separate the gates. Functional, negative, auth and schema cases (1–16) block the merge. Rate limit and latency smoke (17, 20) run on a schedule against a stable environment, because shared environments make them noisy.
A minimal GitHub Actions job for the pytest version:
name: api-tests
on: [pull_request]
jobs:
api:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install pytest requests jsonschema
- run: pytest -m "not scheduled" --junitxml=results.xml
env:
API_BASE: ${{ vars.API_BASE }}
CLIENT_ID: ${{ secrets.CLIENT_ID }}
CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }}
Hand-writing all 22 for every endpoint does not scale past a few dozen operations. That is the job spec-driven generation does well: Shift-Left API reads an OpenAPI document and plans functional, negative, boundary, auth and schema cases per endpoint without needing an AI key, and you review and keep the ones that matter. Either way, the assertions above are the bar each generated test should meet. For more patterns, the API testing complete guide covers strategy, coverage and tooling.
API testing examples FAQ
What is API testing with a real-time example?
Take an online shop's order API. A real-time API test sends POST /v1/orders with a valid basket and checks that the response is 201, that the body has an order id and a total that matches the line items, and that a follow-up GET /v1/orders/{id} returns the same order. The test talks to the API directly over HTTP, with no browser or UI involved, which is what makes it an API test.
What are the main types of API test cases?
Most suites need functional, negative and validation, authentication and authorization, schema or contract, pagination and filtering, reliability (rate limits, idempotency, retries), performance smoke and basic security cases. Functional and negative cases usually make up most of the count.
How many test cases should one API endpoint have?
There is no fixed number, but a typical CRUD endpoint ends up with one or two happy-path cases, three to eight negative and boundary cases, two to four auth cases and one schema check. Endpoints with pagination, filtering or money in the payload need more. Cover every documented status code at least once.
Which tool is best for writing API test examples?
Use what matches your stack. REST Assured suits Java teams, pytest with requests suits Python teams, and Postman test scripts suit teams that already keep collections. The assertions are the same in each; only the syntax changes. Spec-driven tools can also generate many of these cases from an OpenAPI document.
Where can I practise API testing with sample APIs?
Public sandboxes such as JSONPlaceholder, httpbin and Restful-Booker are free to call and good for practising requests and assertions. They don't enforce every rule a real API does, so practise negative, auth and rate-limit cases against an API you control or a local mock.
Sources and further reading
- RFC 9110: HTTP Semantics — status codes, safe and idempotent methods.
- RFC 9457: Problem Details for HTTP APIs — a standard error body shape.
- RFC 6750: OAuth 2.0 Bearer Token Usage — 401 responses and
WWW-Authenticate. - RFC 6585: Additional HTTP Status Codes — 429 Too Many Requests.
- OWASP API Security Top 10 (2023) — BOLA, property-level authorization, resource consumption.
- REST Assured usage guide and JSON schema validation.
- pytest documentation and Requests documentation.
- Postman: writing test scripts.
- OpenAPI Specification 3.1.
Ready to shift left with your API testing?
Import your OpenAPI spec and get a runnable test suite, in our cloud or on your own infrastructure.