Response Processing¶
Response processing is defined as a list of steps, each either a verify or save operation. Steps execute in order.
Response Structure¶
Or using dictionary format for organization:
{
"response": {
"status_check": {"verify": {"status": 200}},
"extract_data": {"save": {"jmespath": {"id": "data.id"}}},
"validate_schema": {"verify": {"body": {"schema": {...}}}}
}
}
Verify Steps¶
Status Code¶
Use template expressions:
Headers¶
Header values are matched by exact, full-string equality — not substring. A
response with Content-Type: application/json; charset=utf-8 does not match
"application/json". Assert the exact value your API returns, or save the header
and check it with an expression (e.g. a startswith/in test) for partial
matches.
{
"verify": {
"headers": {
"Content-Type": "application/json; charset=utf-8",
"X-Request-Id": "{{ request_id }}"
}
}
}
Expression Verification¶
Evaluate template expressions that must return truthy values. Expressions are evaluated against the context — saved variables, fixtures, and substitutions — not the raw HTTP response. Save the data you want to assert on first, then reference it:
{
"response": [
{
"save": {
"jmespath": {
"items": "items",
"status": "status"
}
}
},
{
"verify": {
"expressions": [
"{{ len(items) > 0 }}",
"{{ status == 'ok' }}"
]
}
}
]
}
Response facets that aren't in the JSON body — elapsed time, raw text, individual headers — can't be reached with JMESPath. Capture them with a save user function, which receives the httpx.Response:
# checks.py
import httpx
def response_meta(response: httpx.Response) -> dict:
return {
"response_time": response.elapsed.total_seconds(),
"body_text": response.text,
}
{
"response": [
{"save": {"user_functions": ["checks:response_meta"]}},
{
"verify": {
"expressions": [
"{{ response_time < 1.0 }}",
"{{ 'error' not in body_text }}"
]
}
}
]
}
validate can't see the keys a user function returns, so it reports response_time and body_text as HTTPCHAIN003 ("potentially undefined"). The warning is expected here — the expressions resolve correctly at runtime once the save step has run.
Body Content Checks¶
Contains / Not Contains¶
{
"verify": {
"body": {
"contains": ["success", "user_id"],
"not_contains": ["error", "failed"]
}
}
}
Regex Matching¶
{
"verify": {
"body": {
"matches": ["\"id\":\\s*\\d+", "\"status\":\\s*\"ok\""],
"not_matches": ["\"error\":", "\"failed\":"]
}
}
}
JSON Schema Validation¶
Inline schema:
{
"verify": {
"body": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": {"type": "integer"},
"name": {"type": "string"},
"email": {"type": "string", "format": "email"}
},
"required": ["id", "name"]
}
}
}
}
External schema file:
A relative schema path is not resolved against the scenario file's location.
It is resolved against the current working directory — the directory pytest was
launched from — so "./schemas/user.json" looks for schemas/user.json under
that directory, regardless of where the test file lives. To avoid surprises when
tests are run from a different directory, use an absolute path (for example one
built from a fixture) or run pytest from a consistent project root.
User Function Verification¶
{
"verify": {
"user_functions": [
"mymodule:check_response",
{
"name": "mymodule:check_with_args",
"kwargs": {
"expected_value": "{{ expected }}"
}
}
]
}
}
# mymodule.py
import httpx
def check_response(response: httpx.Response) -> bool:
data = response.json()
return data.get("status") == "ok"
def check_with_args(response: httpx.Response, expected_value: str) -> bool:
return response.json().get("value") == expected_value
Save Steps¶
JMESPath Extraction¶
Extract values from JSON responses:
{
"save": {
"jmespath": {
"user_id": "data.user.id",
"user_name": "data.user.name",
"first_item": "items[0]",
"all_ids": "items[*].id"
}
}
}
Substitutions Save¶
Add computed values to context:
{
"save": {
"substitutions": [
{
"vars": {
"timestamp": "{{ str(now_utc) }}"
}
},
{
"functions": {
"computed": "mymodule:compute_value"
}
}
]
}
}
The template evaluator does not expose datetime, so provide values like timestamps via a fixture and reference the stage with fixtures: ["now_utc"]:
# conftest.py
import pytest
from datetime import datetime
@pytest.fixture
def now_utc():
return datetime.now()
User Function Save¶
Extract data using custom functions:
{
"save": {
"user_functions": [
"mymodule:extract_data",
{
"name": "mymodule:extract_with_args",
"kwargs": {
"key": "specific_field"
}
}
]
}
}
# mymodule.py
import httpx
from typing import Any
def extract_data(response: httpx.Response) -> dict[str, Any]:
data = response.json()
return {
"user_id": data["user"]["id"],
"token": response.headers.get("X-Auth-Token")
}
def extract_with_args(response: httpx.Response, key: str) -> dict[str, Any]:
return {key: response.json().get(key)}
Complete Example¶
{
"stages": [
{
"name": "create_user",
"request": {
"url": "https://api.example.com/users",
"method": "POST",
"body": {
"json": {"name": "Test User", "email": "test@example.com"}
}
},
"response": [
{
"verify": {
"status": 201,
"headers": {
"Content-Type": "application/json; charset=utf-8"
}
}
},
{
"save": {
"jmespath": {
"user_id": "id",
"created_at": "created_at"
}
}
},
{
"verify": {
"body": {
"schema": {
"type": "object",
"required": ["id", "name", "email"]
}
}
}
}
]
},
{
"name": "verify_user",
"request": {
"url": "https://api.example.com/users/{{ user_id }}"
},
"response": [
{
"verify": {
"status": 200,
"expressions": [
"{{ created_at is not None }}"
]
}
}
]
}
]
}