Skip to content

API & Data Test Design Patterns

Request Builder

Fluent API for constructing HTTP requests without coupling tests to raw HTTP semantics.

class UserRequestBuilder:
    def __init__(self) -> None:
        self._payload: dict = {}
        self._headers: dict = {}

    def with_email(self, email: str) -> "UserRequestBuilder":
        self._payload["email"] = email
        return self

    def with_role(self, role: str) -> "UserRequestBuilder":
        self._payload["role"] = role
        return self

    def with_auth_token(self, token: str) -> "UserRequestBuilder":
        self._headers["Authorization"] = f"Bearer {token}"
        return self

    def build(self) -> tuple[dict, dict]:
        return self._payload, self._headers

Usage in tests:

def test_create_admin_user(api_client, admin_token):
    payload, headers = (
        UserRequestBuilder()
        .with_email("admin@example.com")
        .with_role("ADMIN")
        .with_auth_token(admin_token)
        .build()
    )
    response = api_client.post("/users", json=payload, headers=headers)
    assert response.status_code == 201


Response Validator

Centralise assertion logic over HTTP responses. Tests call validators; raw status codes and field checks live in one place.

class ResponseValidator:
    def __init__(self, response) -> None:
        self._response = response

    def is_created(self) -> "ResponseValidator":
        assert self._response.status_code == 201, (
            f"Expected 201, got {self._response.status_code}: "
            f"{self._response.text}"
        )
        return self

    def has_field(self, field: str, value: object) -> "ResponseValidator":
        body = self._response.json()
        assert field in body, f"Field '{field}' missing from response"
        assert body[field] == value, (
            f"Expected {field}={value!r}, got {body[field]!r}"
        )
        return self

    def matches_schema(self, schema: dict) -> "ResponseValidator":
        from jsonschema import validate
        validate(instance=self._response.json(), schema=schema)
        return self

Test Data Builder

Constructs domain objects with unique defaults. Every test gets fresh, isolated data with one line of setup.

import uuid
from dataclasses import dataclass, field


@dataclass
class UserBuilder:
    email: str = field(default_factory=lambda: f"user-{uuid.uuid4().hex[:8]}@test.com")
    role: str = "USER"
    verified: bool = True
    password: str = "Test@1234"

    def as_admin(self) -> "UserBuilder":
        self.role = "ADMIN"
        return self

    def unverified(self) -> "UserBuilder":
        self.verified = False
        return self

    def with_email(self, email: str) -> "UserBuilder":
        self.email = email
        return self

    def build(self) -> dict:
        return {
            "email": self.email,
            "role": self.role,
            "verified": self.verified,
            "password": self.password,
        }

Key properties: - Defaults are unique (UUID-based) — no test pollution - Methods return self for chaining - build() returns a plain dict — no hidden behaviour


Fixture Pattern

pytest fixtures that create, provide, and clean up resources.

import pytest
from framework.clients import UsersClient


@pytest.fixture
def verified_user(users_client: UsersClient) -> dict:
    user = UserBuilder().build()
    created = users_client.create(user)
    yield created
    users_client.delete(created["id"])  # cleanup guaranteed


@pytest.fixture
def admin_user(users_client: UsersClient) -> dict:
    user = UserBuilder().as_admin().build()
    created = users_client.create(user)
    yield created
    users_client.delete(created["id"])

Fixture Scopes

Scope Lifetime Use for
function (default) Per test Mutable resources (users, orders)
module Per file Read-only shared data
session Per run Browser instance, DB connection

Factory Pattern

Factories create configured framework objects — clients, builders, drivers. Tests receive ready-to-use objects without knowing construction details.

class ApiClientFactory:
    @staticmethod
    def create(config: EnvConfig) -> "ApiClient":
        return ApiClient(
            base_url=config.base_url,
            timeout=config.timeout_seconds,
            auth=BearerAuth(config.api_token),
        )
# conftest.py
@pytest.fixture(scope="session")
def api_client(env_config: EnvConfig) -> ApiClient:
    return ApiClientFactory.create(env_config)