Response Validation
Validating API responses is more than checking status codes. A comprehensive response validation strategy verifies status codes, response structure, data types, field presence, pagination correctness, and absence of sensitive data.
Status Codes to Verify
Test both the expected success code and the failure modes:
| Scenario | Expected Code | What to Verify |
|---|---|---|
| Successful GET | 200 | Body contains expected data |
| Successful POST | 201 | Body or Location header contains new resource ID |
| Successful DELETE | 204 | Subsequent GET returns 404 |
| Validation error | 400 or 422 | Error message identifies the invalid field |
| Unauthorized | 401 | Missing or expired token |
| Forbidden | 403 | Valid token, insufficient permissions |
| Not found | 404 | Non-existent resource |
| Conflict | 409 | Duplicate creation attempt |
def test_get_user_success(api, create_user):
user = create_user(name="Alice")
r = api.get(f"/users/{user['id']}")
assert r.status_code == 200
def test_get_nonexistent_user(api):
r = api.get("/users/99999999")
assert r.status_code == 404
def test_create_duplicate_email(api, create_user):
user = create_user(email="unique@test.com")
r = api.post("/users", json={"name": "Duplicate", "email": "unique@test.com"})
assert r.status_code == 409
Response Structure Validation
Verify that the response body contains all expected fields with correct types, and does not contain sensitive data.
def test_list_users_structure(api):
r = api.get("/users")
data = r.json()
# Top-level pagination structure
assert {"items", "total", "page", "per_page"}.issubset(data.keys())
assert isinstance(data["total"], int)
assert isinstance(data["items"], list)
# Individual item structure
if data["items"]:
user = data["items"][0]
assert {"id", "name", "email", "created_at"}.issubset(user.keys())
assert isinstance(user["id"], int)
assert isinstance(user["name"], str)
assert "@" in user["email"]
# No sensitive data
assert "password_hash" not in user
assert "ssn" not in user
assert "credit_card" not in user
def test_user_detail_structure(api, create_user):
user = create_user()
r = api.get(f"/users/{user['id']}")
data = r.json()
# Detailed response may have more fields
required = {"id", "name", "email", "role", "created_at", "updated_at"}
assert required.issubset(data.keys()), f"Missing: {required - data.keys()}"
# Type checking
assert isinstance(data["id"], int)
assert isinstance(data["role"], str)
assert data["role"] in {"admin", "editor", "viewer"}
Schema Validation with jsonschema
For more rigorous validation, use JSON Schema:
from jsonschema import validate
USER_SCHEMA = {
"type": "object",
"required": ["id", "name", "email", "role", "created_at"],
"properties": {
"id": {"type": "integer"},
"name": {"type": "string", "minLength": 1},
"email": {"type": "string", "format": "email"},
"role": {"type": "string", "enum": ["admin", "editor", "viewer"]},
"created_at": {"type": "string", "format": "date-time"},
},
"additionalProperties": False # No unexpected fields
}
def test_user_matches_schema(api, create_user):
user = create_user()
r = api.get(f"/users/{user['id']}")
validate(instance=r.json(), schema=USER_SCHEMA)
Pagination Testing
Pagination is a common source of bugs: overlapping pages, missing items, incorrect totals.
def test_pagination_no_overlap(api):
"""Pages should not contain overlapping items."""
r1 = api.get("/users?page=1&per_page=10")
r2 = api.get("/users?page=2&per_page=10")
ids1 = {u["id"] for u in r1.json()["items"]}
ids2 = {u["id"] for u in r2.json()["items"]}
assert ids1.isdisjoint(ids2), f"Overlapping IDs: {ids1 & ids2}"
def test_pagination_total_consistency(api):
"""Total count should be consistent across pages."""
r1 = api.get("/users?page=1&per_page=10")
r2 = api.get("/users?page=2&per_page=10")
assert r1.json()["total"] == r2.json()["total"]
def test_pagination_all_items_covered(api):
"""Collecting all pages should yield exactly 'total' items."""
all_ids = set()
page = 1
total = None
while True:
r = api.get(f"/users?page={page}&per_page=50")
data = r.json()
total = data["total"]
items = data["items"]
if not items:
break
all_ids.update(u["id"] for u in items)
page += 1
assert len(all_ids) == total
def test_pagination_boundary(api):
"""Request a page beyond the last page."""
r = api.get("/users?page=99999&per_page=10")
assert r.status_code == 200
assert r.json()["items"] == []
def test_pagination_invalid_params(api):
"""Invalid pagination parameters should return errors."""
r = api.get("/users?page=0&per_page=10")
assert r.status_code == 400
r = api.get("/users?page=1&per_page=0")
assert r.status_code == 400
r = api.get("/users?page=-1&per_page=10")
assert r.status_code == 400
Sorting and Filtering Validation
def test_sort_by_created_at_descending(api):
r = api.get("/users?sort=-created_at")
items = r.json()["items"]
dates = [item["created_at"] for item in items]
assert dates == sorted(dates, reverse=True), "Items not sorted by created_at desc"
def test_filter_by_role(api, create_user):
create_user(role="admin")
create_user(role="viewer")
r = api.get("/users?role=admin")
items = r.json()["items"]
assert all(u["role"] == "admin" for u in items), "Filter returned non-admin users"
def test_search_query(api, create_user):
create_user(name="UniqueSearchName123")
r = api.get("/users?q=UniqueSearchName123")
items = r.json()["items"]
assert len(items) >= 1
assert any(u["name"] == "UniqueSearchName123" for u in items)
Response Time Validation
def test_list_users_response_time(api):
r = api.get("/users")
assert r.elapsed.total_seconds() < 2.0, \
f"Response took {r.elapsed.total_seconds():.2f}s (limit: 2s)"
def test_health_check_fast(base_url):
r = requests.get(f"{base_url}/health")
assert r.elapsed.total_seconds() < 0.5
Practical Exercise
Write response validation tests for an API with the following endpoints:
GET /products— returns paginated list with items, total, page, per_pageGET /products/:id— returns single product with id, name, price, categoryPOST /products— creates a product, returns 201 with the new product- Test pagination: no overlap, correct totals, boundary pages
- Test response structure: required fields, correct types, no sensitive data
- Test sorting: verify
sort=pricereturns items in ascending price order
Key Takeaways
- Validate more than status codes: check response structure, types, and sensitive data absence
- Pagination testing catches overlaps, gaps, and incorrect totals
- Use JSON Schema for rigorous structural validation
- Test sorting and filtering to verify query parameters work correctly
- Response time assertions catch performance regressions early