Skip to content

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, Location headers, 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 a Book id-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 /users creates a User resource with fields id and email.
  • GET /users/{userId} requires a userId path parameter.
  • It infers that userId corresponds to the id field 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.

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.

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 Location header is /users/42, the userId parameter becomes 42
  • 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