API Testing: A Practical Walkthrough
- Contributor
- Apr 29
- 5 min read
API tests verify your service from the outside — exercising HTTP endpoints with real requests and checking real responses. They sit between unit tests (your code in isolation) and end-to-end tests (full UI through the stack). They're often the highest-leverage test type: fast enough to run often, realistic enough to catch real bugs, focused enough to maintain.
This guide is the practical walkthrough for an API-centric service.
What to Test
For each endpoint:
Happy path. Valid request produces expected response.
Authentication. Unauthenticated requests are rejected; auth works correctly.
Authorization. Users see only what they should; permissions are enforced.
Validation. Bad inputs produce appropriate error responses.
Boundary conditions. Empty inputs, large inputs, edge values.
Error handling. Server errors produce appropriate responses, not stack traces.
Side effects. Database state, downstream calls, event emissions.
For each business workflow that spans endpoints:
End-to-end flow. Create resource, read it, update it, delete it.
State transitions. Operations in expected sequence work; operations out of sequence are rejected.
Concurrency. Two operations on the same resource handle correctly.
A Working Test Structure
def test_create_user_returns_201_with_user_data():
response = client.post("/users", json={
"name": "Sam",
"email": "sam@example.com"
})
assert response.status_code == 201
body = response.json()
assert body["id"]
assert body["name"] == "Sam"
assert body["email"] == "sam@example.com"
assert "password" not in body # Never leak secrets
Note what the test does:
Makes the HTTP request directly (not through helper that hides details)
Verifies status code explicitly
Verifies expected fields
Verifies what shouldn't be in the response (negative assertion)
Test Organization
Organize by endpoint or resource:
tests/
api/
users/
test_create.py
test_read.py
test_update.py
test_delete.py
test_list.py
workspaces/
...
Each file holds tests for one endpoint. Tests are short and focused.
For workflows that span endpoints, a separate "workflows" or "scenarios" folder:
tests/
api/
scenarios/
test_signup_flow.py
test_workspace_creation_flow.py
Status Codes
Get the status codes right:
200 OK: successful request, returning data
201 Created: successful creation, often with Location header
204 No Content: successful, nothing to return (e.g., DELETE)
400 Bad Request: malformed request
401 Unauthorized: auth missing or invalid
403 Forbidden: auth valid but permission denied
404 Not Found: resource doesn't exist (or user has no permission to know it exists)
409 Conflict: request conflicts with current state
422 Unprocessable Entity: request well-formed but semantically wrong
429 Too Many Requests: rate limited
500 Internal Server Error: something broke; not the client's fault
Tests should verify the specific status code, not just "not 200." assert response.status_code == 400 is better than assert response.status_code != 200.
Authentication in Tests
Most APIs require auth. Two approaches:
Real auth flow: test sets up a user, signs in, gets a token, uses it.
Bypass for tests: test framework provides pre-authenticated client.
The bypass is faster but tests less. A mix often works — use the bypass for most tests, run a few tests through the real auth flow to verify it.
Test Data and Fixtures
API tests typically need data — users, workspaces, resources to operate on.
Patterns:
@pytest.fixture
def test_user(client):
return client.create_user(
name="Test User",
email=f"test+{uuid4()}@example.com"
)
def test_user_can_view_own_profile(client, test_user):
response = client.get(f"/users/{test_user.id}", auth=test_user.token)
assert response.status_code == 200
Fixtures isolate setup. Tests focus on what they're verifying.
Contract Tests
A specialized form: contract tests verify the API's contract with consumers.
def test_create_user_response_matches_schema():
response = client.post("/users", json=valid_user_data)
schema = load_schema("user_create_response.json")
assert matches_schema(response.json(), schema)
Useful when:
Multiple clients consume the API
API changes need backward-compatibility verification
Frontend and backend are separate teams
Tools like Pact formalize this; for simpler cases, JSON schema validation works.
Testing Error Responses
Error responses are often under-tested.
def test_create_user_with_invalid_email_returns_400():
response = client.post("/users", json={
"name": "Sam",
"email": "not-an-email"
})
assert response.status_code == 400
assert "email" in response.json()["errors"]
Test the error response structure, not just the status code. Clients depend on consistent error responses.
Idempotency Testing
For operations that should be idempotent (PUT, DELETE):
def test_delete_is_idempotent():
response1 = client.delete(f"/users/{user_id}")
response2 = client.delete(f"/users/{user_id}")
assert response1.status_code == 204
assert response2.status_code == 204 # Or 404, depending on design
Idempotency assumptions often break in production. Tests pin them down.
Concurrency Testing
API endpoints can be hit concurrently. Tests can verify behavior:
def test_concurrent_creates_dont_duplicate():
import threading
results = []
def create():
results.append(client.post("/users", json=user_data))
threads = [threading.Thread(target=create) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()
# Verify only one succeeded; others got conflict
successes = [r for r in results if r.status_code == 201]
assert len(successes) == 1
Concurrency tests are slower and sometimes flaky, but catch real bugs.
Tools
Common options:
Code-based (recommended for sustained suites): pytest + requests (Python), Jest + supertest (JS), Postman with Newman, REST Assured (Java)
GUI tools (good for exploration, not for suites): Postman, Insomnia, Bruno
Specialized: Karate (Gherkin-flavored), Pact (contract testing)
For ongoing test suites, code-based wins on maintainability. GUI tools are great for exploration and one-off testing.
Speed
API tests should be fast. Targets:
Single test: 50-500ms
Full suite: under 5 minutes for medium-sized service
Speed strategies:
Run in parallel where possible
Use API shortcuts for setup (don't go through UI)
Avoid unnecessary database operations
Mock external services that don't need real verification
Common Mistakes
Testing through the UI when API is direct. Slower, more brittle, no benefit.
Vague assertions. "Response is successful" without checking what it returned.
Missing negative paths. Only testing happy paths; 400/401/403 untested.
Sharing state between tests. Order-dependent failures.
Hardcoded test data. Brittle as the system evolves.
Ignoring response headers. Some headers are part of the contract (Content-Type, Location, rate limits).
Coverage Targets
For an API:
100% endpoint coverage (every endpoint has at least one test)
All major status codes tested per endpoint (200, plus relevant error codes)
Critical workflows have scenario tests
Authentication and authorization are verified, not assumed
100% isn't always achievable, but it's a reasonable target for an API surface.
Maintaining the Suite
As the API evolves:
Versioned endpoints get versioned tests
Deprecated endpoints get tests that verify the deprecation behavior, then deletion when removed
New endpoints get tests before merging the implementation
Breaking changes touch tests that verify the breaking behavior
A test suite that doesn't keep up with the API drifts into uselessness.
Key Takeaway
API tests are some of the highest-value tests for service-oriented codebases. Test each endpoint for happy path, auth, validation, errors, and side effects. Use code-based tests for maintainable suites; GUI tools for exploration. Verify specific status codes and response structure, not vague success. Test error paths and concurrency. Aim for 100% endpoint coverage. The combination of speed, realism, and maintainability makes API tests the workhorse layer of many test strategies.
Related reading
Keep learning. This article is part of the Test Automation path in the ShiftQuality Learning Center. Build test automation that lasts, with ROI you can defend.


