pytest-httpchain¶
A pytest plugin for declarative HTTP API integration testing.
Overview¶
pytest-httpchain is an integration testing framework for HTTP APIs based on httpx.
It helps with common HTTP API testing scenarios where you need to make several calls in specific order using data obtained along the way, like auth tokens or resource IDs.
Why pytest-httpchain?¶
Testing HTTP APIs with plain pytest often leads to these pain points:
- Boilerplate accumulates — Every test repeats the same setup: create client, set headers, make request, parse response, assert. The actual test intent gets buried.
- Data threading is manual — When one call returns a token or ID needed by the next, you end up with fragile helper functions passing state around.
- Common patterns get copy-pasted — Auth flows, base URLs, shared headers end up duplicated across test files.
- Code reviews are noisy — The actual test logic is rarely clear because of all the boilerplate.
pytest-httpchain offers a more structured approach.
Features¶
Declarative JSON Format¶
Test scenarios are JSON documents that describe what to test, not how. No setup code to scroll through — the request and assertions are right there. Comments and trailing commas are welcome in every scenario and every file it pulls in; name a file test_<name>.http.jsonc and editors treat it as JSON with comments.
$include / $merge / $ref with Deep Merging¶
Reuse arbitrary parts of your scenarios with JSON references. Properties merge with type checking, so you can compose scenarios from shared fragments (auth flows, common headers, base URLs). Use $include or $merge for better IDE support.
Multi-Stage Execution¶
Each scenario contains 1+ stages executed in order. One stage failure stops the chain. Use always_run for cleanup stages that should execute regardless, and skip_if to skip a stage on a condition known only once the chain is running, such as a value an earlier stage saved.
Retries and Polling¶
A stage's retry attempts it again while it fails, after a wait that can grow each time: poll an asynchronous job until it is done, or ride out eventual consistency and a flaky network. Only the attempt that passes saves anything.
Common Data Context¶
A key-value store persists throughout scenario execution. Variables, fixtures, and saved response data all live here. Use Jinja-style expressions ({{ var }}) in any request value. Built-in functions give the values tests keep needing without a fixture: the time (now(), timestamp()), base64, JSON and URL encoding, and SHA-256, MD5 and HMAC-SHA256 digests for signing a request.
Request Bodies¶
JSON, form, XML, text, base64, a binary file and GraphQL, and multipart uploads that mix form fields with files: each file read from a path or given inline, several under one name if need be, with its own filename and content type.
Response Processing¶
- JMESPath — Assert on values in JSON responses directly (
"jmespath": {"data.id": 42, "items": {"length": 3}}), or extract them for later stages - Regex — Save values from bodies that are not JSON, such as a CSRF token from an HTML form (
"regex": {"csrf": "name=\"csrf\" value=\"([^\"]+)\""}) - JSON Schema — Validate response structure against a schema, inline or from a file, or one inside a document you already have:
"schema": "./openapi.json#/components/schemas/User"checks the response against an OpenAPI component, its$refs resolved across the document and into local files, never over the network - User functions — Call Python functions for custom extraction, verification, or authentication
- Failure reports — A failing verify step lists every check that failed, not only the first, and the report gives the request as a ready-to-run
curlcommand beside the request and response it shows
Scenario-wide Client Settings¶
A scenario's client block sets up the HTTP client all its stages share, once: a base URL their relative URLs are appended to, headers and query parameters sent with every request, timeout, redirects, proxy, HTTP/2 and connection pool. A stage overrides what it needs.
Authentication¶
Basic, digest and bearer authentication are built in, for the whole scenario or one request: "auth": {"bearer": "{{ token }}"} sends the token a login stage saved. "auth": false exempts a public endpoint, and a Python function covers any other scheme.
Parametrization¶
Run stages with different parameter values, similar to pytest's @pytest.mark.parametrize.
Parallel Execution¶
Execute multiple requests concurrently for load testing, stress testing, or bulk operations. With collect_saves, every request's saved values are kept as lists, so a later stage can delete every resource a parallel stage created. The report sums a parallel stage up (iterations passed and failed, throughput, p50/p95/p99 latency), and thresholds fail it below a success ratio or above a latency.
Import Recorded Traffic¶
Start from traffic you already have: pytest-httpchain import turns a browser's HAR export, or curl commands from an API's docs or a failing stage's report, into a scenario, a stage per request. Base URL, query parameters, JSON, form and multipart bodies and Basic or Bearer credentials map into the dialect, and no secret is written: tokens, passwords and cookies become placeholders read from environment variables.
Full pytest Integration¶
Markers, fixtures, parametrization, and other plugins work as expected. You're not locked into a separate ecosystem.
Quick Example¶
{
"substitutions": [
{"vars": {"user_id": 1}}
],
"stages": {
"get_user": {
"request": {
"url": "https://api.example.com/users/{{ user_id }}"
},
"response": [
{"verify": {"status": 200}},
{"save": {"jmespath": {"user_name": "name"}}}
]
},
"update_user": {
"request": {
"url": "https://api.example.com/users/{{ user_id }}",
"method": "PUT",
"body": {
"json": {"name": "{{ user_name }}_updated"}
}
},
"response": [
{"verify": {"status": 200}}
]
}
}
}
Installation¶
See Getting Started for detailed installation and configuration instructions.
AI agent support¶
pytest-httpchain ships a scenario validator to help AI agents (and humans) author and check test scenarios.
Validate scenario files for structure and common problems (undefined variables, variables used before they are saved, duplicate stage names, fixture conflicts, no-op verify steps, contradictory body checks); every finding carries a stable HTTPCHAINxxx diagnostic code, and it exits non-zero on failure, so it works as a CI gate. Name the files, or a directory to check every scenario pytest would collect in it:
uvx pytest-httpchain validate tests/test_login.http.json
uvx pytest-httpchain validate tests/
# machine-readable output for editors / CI:
uvx pytest-httpchain validate --format json tests/test_login.http.json
These checks also run automatically at pytest collection time: semantic errors fail collection and warnings are reported, so pytest --collect-only validates your whole suite.
Add --deep for opt-in checks that import your module:func references (verifying they resolve and their call signatures match, including the injected response) and confirm referenced files/schemas exist. It imports your code, so it never runs at collection time; combine with --strict (fail on warnings) and --syspath (extra import roots):
Editors get as-you-type validation and autocomplete via the published JSON Schema — reference it with a $schema key in your test files.
Several read-only commands help inspect scenarios offline — emit the schema matching your installed version, resolve references, summarize the variable data-flow, or render it as a Mermaid diagram:
uvx pytest-httpchain schema > scenario.schema.json
uvx pytest-httpchain resolve tests/test_login.http.json
uvx pytest-httpchain show tests/test_login.http.json
uvx pytest-httpchain graph tests/test_login.http.json
And import writes a starter scenario, which passes validate, from recorded traffic:
uvx pytest-httpchain import har session.har -o tests/test_checkout.http.json
uvx pytest-httpchain import curl -o tests/test_orders.http.json 'curl https://api.example.com/v1/orders'
Every command and option is documented in the CLI reference.
Thanks¶
This project was inspired by Tavern and pytest-play.
httpx does comms. Pydantic keeps structure. simpleeval powers templates. pytest-datadir saved a lot of elbow grease while testing.