QA Engineer Skills 2026QA-2026Response Validation

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:

  1. GET /products — returns paginated list with items, total, page, per_page
  2. GET /products/:id — returns single product with id, name, price, category
  3. POST /products — creates a product, returns 201 with the new product
  4. Test pagination: no overlap, correct totals, boundary pages
  5. Test response structure: required fields, correct types, no sensitive data
  6. Test sorting: verify sort=price returns 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