Skip to content

Ananto30/contractest

Repository files navigation

Contractest

Generate API contracts from real service interactions and test that services still match those contracts.

Installation

uv venv
uv sync

Record Contracts (Using the Recorder)

The recorder acts as a proxy between a client (Service A) and the target service (Service B). It records all API interactions to generate contracts.

  • Set up: Place Service B on port 7777 (configurable in conf.toml as target_service_url)

  • Start the recorder:

    uv run -m contractest.recorder
  • Send requests through the recorder from Service A (frontend, webapp, etc.)

  • The recorder automatically records all contracts during interactions

  • Stop the recorder (Ctrl+C) and contracts are saved to ./contracts (configurable in conf.toml)

Define Flows for Multi-Step API Testing

Flows let you chain API calls together by extracting values from one response and injecting them into the next request (e.g., login → save token → use token in next call).

Workflow

  1. Record contracts with the recorder (creates basic empty flows)

  2. List recorded contracts:

    uv run contractest/common/flow_helper.py
  3. Inspect a contract to see request/response:

    uv run contractest/common/flow_helper.py inspect <contract_hash>
  4. Edit contracts/flow.yaml manually to define data flows:

    - path: /auth/login
      method: POST
      contract_hash: "a1b2c3d4e5f6g7h8"
      store:
        - key: "auth_token"
          parameter_position: "body"
          parameter_name: "access_token"
      use: []
    
    - path: /api/users/me
      method: GET
      contract_hash: "b2c3d4e5f6g7h8i9"
      store: []
      use:
        - key: "auth_token"
          parameter_position: "header"
          parameter_name: "Authorization"

Flow Definition

  • store — Extract values from response to use in later calls

    • key — Variable name to save as
    • parameter_position — Where to find it (body, header, cookie)
    • parameter_name — JSON path or field name
  • use — Inject stored values into request

    • key — Which variable to use
    • parameter_position — Where to put it (body, header, path, query, cookie)
    • parameter_name — Target location (field name, header name, or {placeholder} for path)

Example: Login → Get User → Update User

# Step 1: Login and extract token
- path: /auth/login
  method: POST
  contract_hash: "hash1"
  store:
    - key: "access_token"
      parameter_position: "body"
      parameter_name: "token"
  use: []

# Step 2: Get user (using token)
- path: /users/me
  method: GET
  contract_hash: "hash2"
  store:
    - key: "user_id"
      parameter_position: "body"
      parameter_name: "id"
  use:
    - key: "access_token"
      parameter_position: "header"
      parameter_name: "Authorization"

# Step 3: Update user (using token and user_id)
- path: /users/{user_id}
  method: PATCH
  contract_hash: "hash3"
  store: []
  use:
    - key: "access_token"
      parameter_position: "header"
      parameter_name: "Authorization"
    - key: "user_id"
      parameter_position: "path"
      parameter_name: "{user_id}"

Validate Service Against Contracts (Using the Validator)

The validator tests that a service conforms to previously recorded contracts. Use this to ensure API compatibility hasn't broken.

  • Start the service under test (Service B) on the configured URL in conf.toml as service_under_test_url

  • Run the validator:

    uv run -m contractest.validator

The validator will test all recorded contracts against the live service and report any discrepancies.

Mock Server for Frontend Testing

Use the mock server to test your frontend against recorded API contracts without needing the real backend running. Perfect for Playwright or other browser automation tests.

Start the Mock Server

uv run -m contractest.mock_server

The mock server will:

  • Load recorded contracts from ./contracts (configurable in conf.toml)
  • Start on localhost:8080 (configurable as host and port)
  • Serve responses matching recorded API behavior

Example output:

Mock server started on localhost:8080
Serving 5 contracts

Available endpoints:
  GET    /api/users
  POST   /api/users
  GET    /api/orders
  DELETE /api/orders/123
  PATCH  /api/orders/123

Use with Frontend Tests

Configure your frontend tests to point to the mock server:

// Playwright example
const BASE_URL = 'http://localhost:8080';

test('should fetch users', async ({ page }) => {
  await page.goto(`${BASE_URL}/users`);
  // ... your test assertions
});

The mock server will serve the exact responses that were recorded from the real backend, allowing your frontend to test against realistic data.

Handling Dynamic Fields

APIs often contain fields that change over time or are unique per request (timestamps, IDs, tokens, etc.). Contractest supports ignoring these fields in both request and response validation.

Configuration

Edit conf.toml to ignore dynamic fields:

# Ignore fields in REQUEST bodies (checked only for structure/type)
[request_body_comparison.ignore_fields_by_path]
"/delivery" = ["delivery_date", "order_id"]
"/users" = ["id", "created_at"]

# Ignore fields in RESPONSE bodies (checked with full validation)
[response_body_comparison.ignore_fields]
# Globally ignored fields in all responses
# ignore_fields = ["timestamp", "nonce", "correlation_id"]

[response_body_comparison.ignore_fields_by_path]
"/login" = ["session_token", "auth_token"]
"/orders" = ["order_id"]

Common Dynamic Fields to Ignore

Request fields (typically ignored by structure only):

  • delivery_date, start_date - dates that must be current
  • order_id, user_id - IDs that vary per request
  • timestamp, request_time - request timestamps

Response fields (typically ignored globally):

  • id, uuid - generated identifiers
  • created_at, updated_at - automatic timestamps
  • session_token, auth_token - authentication tokens
  • nonce, correlation_id - one-time values
  • expires_at - expiration times

Comparison Modes

Control how deeply the validator checks responses:

[response_body_comparison]
strict_match = false              # Allow flexible matching
value_match = true                # Check field values match
structure_match = true            # Check fields exist (types/names)
array_order_match = true          # Arrays must be in same order
array_length_match = false        # Allow different array lengths

Generating Dynamic Request Values

Instead of ignoring request fields, you can generate dynamic values using Python expressions. This is useful when the API requires current dates, unique IDs, or other dynamic values.

Available functions and modules:

  • datetime - datetime.date.today(), datetime.datetime.now(), etc.
  • uuid - uuid.uuid4(), uuid.uuid1(), etc.
  • random - random.randint(), random.choice(), etc.
  • str, int, float - type conversion functions

Configuration:

[request_field_generators]
# Format: "/path" = { field_name = "python_expression" }

"/delivery" = { delivery_date = "datetime.date.today().isoformat()" }
"/orders" = { order_id = "str(uuid.uuid4())", item_count = "random.randint(1, 10)" }
"/users" = { created_at = "datetime.datetime.now().isoformat()" }

Examples:

[request_field_generators]
# Current date (e.g., "2024-01-15")
"/api/delivery" = { delivery_date = "datetime.date.today().isoformat()" }

# Unique ID (e.g., "550e8400-e29b-41d4-a716-446655440000")
"/api/orders" = { order_id = "str(uuid.uuid4())" }

# Random value (e.g., 5)
"/api/items" = { quantity = "random.randint(1, 10)" }

# ISO format timestamp (e.g., "2024-01-15T10:30:45.123456")
"/api/events" = { timestamp = "datetime.datetime.now().isoformat()" }

When validation runs, these expressions are evaluated fresh for each test, so you always get current/unique values.

About

Generate (contract) tests by browsing your app or web

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors