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
requestsbut 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
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.