Skip to content

03 โ€” API Testing: HTTPX, Contract vs Schema Validation

JD: "REST API testing using Postman, HTTPX, or equivalent for contract and schema validation." The high-value distinction the experts will probe: contract testing vs schema validation โ€” many candidates conflate them.


1. HTTPX basics (the Pythonic requests)

In plain words: HTTPX is a modern HTTP client โ€” like requests but with async support and HTTP/2. It's the natural fit alongside Pytest.

import httpx

def test_get_account(client):                 # client is a fixture (see file 01)
    r = client.get("/accounts/123")
    assert r.status_code == 200
    body = r.json()
    assert body["id"] == "123"
    assert r.headers["content-type"].startswith("application/json")
    assert r.elapsed.total_seconds() < 1.0     # latency budget

# async version โ€” agent backends are async
async def test_async(async_client):
    r = await async_client.post("/quote", json={"symbol": "AAPL"})
    assert r.status_code == 201

Reusable clients:

@pytest.fixture(scope="session")
def client():
    with httpx.Client(base_url=BASE_URL, headers={"Authorization": f"Bearer {TOKEN}"},
                      timeout=10.0) as c:
        yield c


2. Schema validation โ€” "is this payload the right shape?"

In plain words: schema validation checks a single response body against a declared structure โ€” required fields, types, formats, constraints. It answers: does this JSON conform?

Pydantic (best for Python stacks)

from pydantic import BaseModel, Field, EmailStr
from datetime import datetime

class Account(BaseModel):
    id: str
    balance: float = Field(ge=0)              # non-negative
    currency: str = Field(pattern="^[A-Z]{3}$")
    owner_email: EmailStr
    created_at: datetime

def test_account_schema(client):
    r = client.get("/accounts/123")
    account = Account.model_validate(r.json())   # raises ValidationError if it doesn't fit
    assert account.balance >= 0

Pydantic gives type coercion + rich constraints + clear errors and doubles as your test-data model.

JSON Schema (language-agnostic, good when the API publishes one)

import jsonschema

SCHEMA = {
    "type": "object",
    "required": ["id", "balance", "currency"],
    "properties": {
        "id": {"type": "string"},
        "balance": {"type": "number", "minimum": 0},
        "currency": {"type": "string", "pattern": "^[A-Z]{3}$"},
    },
    "additionalProperties": False,       # reject unexpected fields (contract tightening)
}

def test_json_schema(client):
    jsonschema.validate(instance=client.get("/accounts/123").json(), schema=SCHEMA)

3. Contract testing โ€” "do two services still agree?"

In plain words: a contract is the agreed request/response shape between a consumer and a provider. Contract testing verifies the provider still honours what consumers expect (and vice-versa) โ€” so a backend change doesn't silently break the Angular UI or another service. Schema validation is one check; contract testing is the discipline of keeping the interface stable across independently-deployed services.

Schema validation Contract testing
Question Does this payload match a shape? Do consumer & provider still agree on the interface?
Scope One response The relationship between two services
Catches Malformed/wrong-type payload Breaking changes (renamed field, removed endpoint, changed status) before deploy
Tooling Pydantic, JSON Schema Pact (consumer-driven), OpenAPI/Spectral, Schemathesis

Consumer-driven contracts (Pact idea)

  • The consumer (e.g. Angular UI or another service) declares "when I GET /accounts/{id} I expect these fields/types" โ†’ produces a pact file.
  • The provider replays those expectations in CI. If the provider changes and breaks them, the provider build fails โ€” before it reaches production.

OpenAPI as the contract

If the platform has an OpenAPI/Swagger spec, validate live responses against it and even auto-generate tests:

# Schemathesis: property-based tests generated from the OpenAPI spec
schemathesis run http://localhost:8000/openapi.json --checks all
This fuzzes every endpoint against its declared contract and surfaces mismatches, 500s, and schema violations you didn't hand-write.

Interview line: "Schema validation asserts a single payload conforms; contract testing keeps two independently-deployed services in agreement over time. I'd validate individual responses with Pydantic, and protect the service boundary with consumer-driven contracts (Pact) or by validating against the OpenAPI spec โ€” run as a CI gate so a breaking change fails the provider build, not production."


4. The test cases interviewers expect (beyond happy path)

# Status codes across the lifecycle
def test_not_found(client):        assert client.get("/accounts/nope").status_code == 404
def test_bad_request(client):      assert client.post("/quote", json={}).status_code == 422
def test_unauthorized():           assert httpx.get(f"{BASE}/accounts/1").status_code == 401

# Auth: valid, expired, wrong-scope token
# Idempotency: same POST with Idempotency-Key twice -> one resource
def test_idempotent_create(client):
    key = {"Idempotency-Key": "abc-123"}
    a = client.post("/payments", json=PAY, headers=key)
    b = client.post("/payments", json=PAY, headers=key)
    assert a.json()["id"] == b.json()["id"]

# Pagination, sorting, filtering
# Boundary/negative: oversized body, wrong types, injection strings
# Rate limiting: 429 + Retry-After header present
# Error body shape: errors are ALSO a contract โ€” validate them too

Senior signal: error responses are part of the contract. Validate the error schema ({"code","message","correlationId"}), not just the 4xx status. The correlationId links to the audit trail (file 12).


5. Contract/data validation for agent APIs

Agent endpoints add fields you must validate structurally even though the content is non-deterministic:

class AgentResponse(BaseModel):
    trace_id: str                      # must exist -> links to Langfuse (file 09)
    final_answer: str
    tool_calls: list[dict]             # the path taken
    confidence: float = Field(ge=0, le=1)
    model_version: str                 # so you can detect regression (file 10)

def test_agent_response_contract(client):
    resp = AgentResponse.model_validate(client.post("/agent/invoke", json=REQ).json())
    assert resp.trace_id                      # structural: always present
    assert 0 <= resp.confidence <= 1
    assert resp.model_version == EXPECTED_MODEL
    # content correctness is validated separately with semantic/eval methods (file 07)

Interview line: "For agent APIs I split validation: the envelope (trace_id, tool_calls, confidence, model_version) is a hard schema contract I assert exactly; the natural-language content is validated probabilistically with semantic checks or an eval โ€” never exact-match."


6. Postman / Newman (still asked)

  • Collections group requests; environments hold variables (base URL, token).
  • Tests in the "Tests" tab (pm.test, pm.expect), pre-request scripts for auth/setup.
  • Newman runs collections in CI: newman run collection.json -e env.json --reporters cli,junit.
  • Good for quick exploratory + shareable smoke suites; for maintainable code-owned suites at scale, HTTPX+Pytest wins (versioned, DRY, fixtures).

Rapid-fire recall

  • Schema validation = one payload conforms (Pydantic / JSON Schema).
  • Contract testing = two services still agree (Pact / OpenAPI / Schemathesis), run as a CI gate.
  • Validate error bodies and headers, not just status.
  • Idempotency, auth variants, rate-limit, pagination = expected coverage.
  • Agent APIs: hard-assert the envelope, probabilistically validate the content.
  • Newman for Postman-in-CI; HTTPX+Pytest for code-owned suites.