Understanding Stateful Testing
Learn how Schemathesis's stateful testing works, when to use it, and how it fits into your testing strategy.
What is Stateful Testing?
Stateful testing chains API calls together using real data from responses, rather than testing each operation independently.
Without stateful testing:
POST /users → Creates user → Test passes ✓
GET /users/123 → Uses random ID → 404 Not Found ✗
DELETE /users/456 → Uses random ID → 404 Not Found ✗
Each operation tested alone. Random data doesn't match real resources.
With stateful testing:
POST /users → Creates user → Returns ID: 789 ✓
GET /users/789 → Uses actual ID from POST → 200 OK ✓
DELETE /users/789 → Uses actual ID from POST → 200 OK ✓
Operations chained together. Real IDs from real responses.
How It Works
Schemathesis analyzes your schema to understand how operations connect. The mechanism differs by spec but the model is the same: identify producers (operations that return resources), identify consumers (operations that need resources), and chain them.
Step 1: Find resources
Identifies what your API produces:
POST /users → Creates "User" resource
POST /orders → Creates "Order" resource
Step 2: Match parameters
Links parameters to resources:
GET /users/{userId} → Needs "userId" from User resource
GET /orders/{orderId} → Needs "orderId" from Order resource
Step 3: Connect operations
Chains operations that share resources:
POST /users (creates User with id=123)
↓ pass id as userId
GET /users/{userId} (needs User)
↓ pass id as userId
PUT /users/{userId} (needs User)
↓ pass id as userId
DELETE /users/{userId} (needs User)
Step 4: Generate test scenarios
Runs random workflows:
- Create user -> Get user -> Update user -> Delete user
- Create user -> Create order for that user -> Get order
- Create user -> Delete user -> Get user
Reusing Response Data in Non-Stateful Testing
When fuzzing GET /users/{id} with random IDs, nearly every request returns 404. Error handling gets thoroughly tested, but success logic — response schema validation, data serialization, permission checks—remains largely untouched because valid IDs are astronomically rare in random generation.
Schemathesis captures useful values from successful responses and reuses them when generating test cases. Dependency analysis identifies which operations produce resources and which consume them. For example, it recognizes that POST /users creates users with IDs, and GET /users/{id} needs those IDs.
During fuzzing, captured values augment random generation. GET /users/{id} tests with both random IDs (finding 404 handling bugs) and real IDs from earlier POST /users calls (finding bugs in success paths).
This works across non-stateful test phases and within them. The examples phase both contributes to and draws from the pool - a POST /users example populates the pool, and GET /users/{id} examples use the captured ID to reach code paths that random IDs never hit. Within fuzzing itself, early test cases discover values that later cases use.
Connecting Operations
Stateful testing needs to know how operations relate. For example, POST /users creates a user, and GET /users/{userId} needs that user's ID.
Schemathesis discovers these connections per spec:
- OpenAPI: schema analysis,
Locationheaders, and explicit OpenAPI Links (see below). - GraphQL: the type graph itself encodes the connections — a mutation returning
Book!is automatically connected to a query taking aBookid-typed argument.
1. Automatic Schema Analysis (OpenAPI)
Analyzes your OpenAPI schema to detect connections.
Example: Your schema has:
paths:
/users:
post:
responses:
'201':
content:
application/json:
schema:
properties:
id: {type: string}
email: {type: string}
/users/{userId}:
get:
parameters:
- name: userId
in: path
Schemathesis detects the following relationships:
POST /userscreates aUserresource with fieldsidandemail.GET /users/{userId}requires auserIdpath parameter.- It infers that
userIdcorresponds to theidfield returned by the POST response. - Therefore, it can build a sequence:
POST /users->GET /users/{userId}.
Works for:
- Path parameters:
userId,user_id,{id}in/users/{id} - Nested resources:
/users/{userId}/posts - Pagination:
{"data": [...]},{"items": [...]} - Schema composition:
allOf,oneOf,anyOf
2. Automatic Schema Analysis (GraphQL)
For GraphQL, Schemathesis reads the type graph directly. Object types with an id field become resources; mutations returning those types become producers; queries and mutations whose id-typed arguments resolve to those types become consumers.
Example:
type Book { id: ID! title: String! }
type Mutation {
addBook(title: String!): Book! # Producer of Book
deleteBook(id: ID!): Boolean # Cleanup of Book
}
type Query {
book(id: ID!): Book # Consumer of Book
}
Schemathesis builds the chain addBook -> book (and addBook -> deleteBook) automatically.
Argument-name conventions are recognized: bookId, book_id, bookIds all resolve to the Book type. Bespoke <Type>ID scalars (e.g. BookID) work the same way.
3. Location Header Learning
While running tests, Schemathesis can also learn new connections dynamically by observing Location headers in responses.
If your API returns a Location header when creating resources, Schemathesis automatically discovers follow-up operations for that resource.
POST /users → 201 Created
Location: /users/123
# Learns: GET /users/123, PUT /users/123, DELETE /users/123
This mechanism requires your API to return a valid Location header in 201 Created responses.
4. Manual OpenAPI Links
When you want full control or need to specify non-path relationships, you can define explicit connections using OpenAPI Links.
paths:
/users:
post:
responses:
'201':
links:
GetUser:
operationId: getUser
parameters:
userId: '$response.body#/id'
This explicitly tells Schemathesis that the userId parameter in the getUser operation should be populated from the id field in the response body of the POST /users operation.
Use manual links when:
- Automatic schema analysis misses a connection
- You want precise, explicit control over operation relationships
All Three Work Together
Schema analysis runs first, manual links override when present, and Location learning adds runtime discoveries.
How Schemathesis Extends OpenAPI Links
Regex Extraction from Headers and Query Parameters
Standard OpenAPI links can extract string data from various places, but only exact values. Schemathesis adds regex support for pattern-based extraction for a part of a string:
paths:
/users:
post:
responses:
'201':
headers:
Location:
schema:
type: string
links:
GetUserByUserId:
operationId: getUser
parameters:
userId: '$response.header.Location#regex:/users/(.+)'
How it works:
- If
Locationheader is/users/42, theuserIdparameter becomes42 - The regex must be valid Python regex with exactly one capturing group
- If regex doesn't match, the parameter is set to empty string
Enhanced RequestBody Support
OpenAPI standard does not allow recursive expressions in requestBody:
SetManagerId:
operationId: setUserManager
# only plain or embedded expression or literals
requestBody: "$response.body#/id"
Schemathesis allows for nested expressions:
SetManagerId:
operationId: setUserManager
requestBody: {
"user_id": "$response.body#/id",
"metadata": {
"created_by": "$response.body#/author",
"tags": ["$response.body#/category", "static-value"]
}
}
If response body is {"id": 123, "author": "alice", "category": "blog"}, the request body becomes:
{
"user_id": 123,
"metadata": {
"created_by": "alice",
"tags": ["blog", "static-value"]
}
}
Backwards Compatibility
OpenAPI 2.0 Support: Use x-links extension with identical syntax:
# OpenAPI 3.0
links:
GetUser: ...
# OpenAPI 2.0
x-links:
GetUser: ... # Same syntax, including regex support