Skip to main content

API Testing: Contracts, State, Authorization, and Useful Failure Evidence

Build API tests that verify persisted outcomes, isolate test data, handle cleanup, and distinguish timing checks from load tests.

3 min read
API Testing: Contracts, State, Authorization, and Useful Failure Evidence
On this page

An API returning 200 is the start of an assertion, not the end. Verify the response contract, the stored state, and the caller’s authority. Keep most local business-rule tests close to the code; use API and contract tests at service boundaries and a smaller set of browser journeys for critical user flows. The distribution depends on system risk, not a fixed pyramid percentage.

Give each scenario an isolated lifecycle

The integration example below assumes a staging API where POST /items returns 201 with an id, GET returns the stored item, and DELETE returns 204. Supply a session authorized for a synthetic test account. Replace this contract with your service’s documented behavior.

python
from uuid import uuid4

def test_item_lifecycle(session, base_url):
    collection = base_url.rstrip("/") + "/items"
    payload = {"name": f"test-{uuid4().hex}", "price": 199}
    created = session.post(collection, json=payload, timeout=10)
    created.raise_for_status()
    item_id = created.json()["id"]
    item_url = f"{collection}/{item_id}"
    try:
        assert created.status_code == 201
        response = session.get(item_url, timeout=10)
        assert response.status_code == 200
        stored = response.json()
        assert stored["id"] == item_id
        assert stored["name"] == payload["name"]
        assert stored["price"] == payload["price"]
    finally:
        deleted = session.delete(item_url, timeout=10)
        assert deleted.status_code == 204

If creation succeeds but returns malformed JSON, this example cannot discover the identifier for cleanup. Real fixtures should also support cleanup by the unique test prefix or an isolated tenant. Report cleanup failures alongside the original assertion failure so neither is lost.

Keep the HTTP wrapper small

python
import requests

class APIClient:
    def __init__(self, base_url):
        self.base_url = base_url.rstrip("/")
        self.session = requests.Session()

    def request(self, method, path, **kwargs):
        kwargs.setdefault("timeout", 10)
        url = self.base_url + "/" + path.lstrip("/")
        return self.session.request(method, url, **kwargs)

    def close(self):
        self.session.close()

A fixture should close the client and supply credentials through approved configuration. Log request identifiers, status, and sanitized response details. Avoid recording authorization headers or complete customer payloads. A Requests timeout bounds connection/read waiting; it is not a universal total-duration deadline.

Test the boundary, not just the happy path

AreaUseful assertion
ContractRequired fields, types, nullability, allowed values, and backwards compatibility.
AuthorizationUser A cannot read or mutate user B’s resource; assert the status defined by the service contract.
ValidationMalformed input is rejected and no unintended record is created.
IdempotencyRepeated requests with the documented key produce the specified result without duplicate side effects.
ConcurrencyCompeting updates obey the expected version or conflict policy.

Separate latency observations from load tests

python
from time import perf_counter

def check_health(session, base_url, budget_seconds):
    started = perf_counter()
    response = session.get(base_url.rstrip("/") + "/health", timeout=10)
    response.raise_for_status()
    elapsed = perf_counter() - started
    assert elapsed < budget_seconds
    return elapsed

This measures a single request through response-body consumption. Choose a budget from the environment and service requirement. A sequential loop is a smoke check, not a realistic load test: use a load tool with explicit concurrency, arrival rate, warmup, and percentile reporting when capacity is the question.

Requests session and timeout documentation.

Dhiraj Das

About the Author

Dhiraj Das is an Automation Consultant with over a decade of experience building systems that expose failures, reduce flakiness, and make complex workflows repeatable. He applies that discipline to AI-agent validation, LLM testing, and postmortems.

He shares small open source utilities from real automation work, including: waitless (flaky tests), sb-stealth-wrapper (bot detection), selenium-teleport (state persistence), selenium-chatbot-test (AI chatbot testing), lumos-shadowdom (Shadow DOM), and visual-guard (visual regression).

Share this article: