Skip to content

Fixture

Category: Testing Pattern

Overview

A fixture provides a fixed baseline state for tests to run against. Fixtures encapsulate test setup and teardown logic, enabling reusable test data and environment configuration. This pattern separates test preparation from test assertions, improving test clarity and reducing duplication.

Concepts

Test Fixture

Setup and teardown mechanism that establishes a known state before each test runs and optionally cleans up afterward. Commonly implemented through lifecycle methods (setUp/tearDown) or dependency injection (pytest fixtures).

Data Fixture

Predefined data sets used to populate databases, configure services, or provide sample inputs. Data fixtures ensure tests run against consistent, predictable data.

Object Mother / Test Data Builder

Factory patterns specialized for creating complex test objects with sensible defaults. Object Mother provides static factory methods, while Test Data Builder uses a fluent interface for customization.

Usage Guidelines

Use when:

  • Multiple tests require the same setup or test data
  • Test setup logic is complex enough to obscure test intent
  • Tests need isolation from external dependencies (databases, APIs)
  • You want to share test infrastructure across a test suite

Avoid when:

  • Each test has unique setup requirements with no overlap
  • Tests are simple enough that inline setup improves readability
  • The fixture abstraction hides important test context
  • Setup/teardown overhead exceeds the benefit for trivial tests

Implementation

Test Fixture (Context Manager)

from contextlib import contextmanager
from dataclasses import dataclass, field
from typing import Any, Generator


@dataclass
class FixtureContext:
    """Container for test state managed by a fixture."""
    data: dict[str, Any] = field(default_factory=dict)
    resources: list[Any] = field(default_factory=list)


@contextmanager
def database_fixture() -> Generator[FixtureContext, None, None]:
    """Fixture that sets up and tears down a test database connection."""
    context = FixtureContext()
    # Setup: create test database connection
    context.data["db"] = {"connected": True, "tables": []}
    context.resources.append("db_connection")

    try:
        yield context
    finally:
        # Teardown: close connection and cleanup
        context.data["db"]["connected"] = False
        context.resources.clear()

Data Fixture

from dataclasses import dataclass


@dataclass
class User:
    """Domain object representing a user."""
    id: int
    name: str
    email: str
    role: str = "user"


class UserFixtures:
    """Predefined user data for testing."""

    @staticmethod
    def admin_user() -> User:
        """Returns a standard admin user for testing."""
        return User(id=1, name="Admin", email="admin@example.com", role="admin")

    @staticmethod
    def regular_user() -> User:
        """Returns a standard regular user for testing."""
        return User(id=2, name="John Doe", email="john@example.com", role="user")

    @staticmethod
    def users_list() -> list[User]:
        """Returns a list of test users."""
        return [
            UserFixtures.admin_user(),
            UserFixtures.regular_user(),
            User(id=3, name="Jane Doe", email="jane@example.com"),
        ]

Test Data Builder

from dataclasses import dataclass


@dataclass
class Order:
    """Domain object representing an order."""
    id: int
    customer_id: int
    items: list[str]
    total: float
    status: str


class OrderBuilder:
    """Fluent builder for creating Order test objects with sensible defaults."""

    def __init__(self) -> None:
        self._id: int = 1
        self._customer_id: int = 100
        self._items: list[str] = ["default_item"]
        self._total: float = 0.0
        self._status: str = "pending"

    def with_id(self, order_id: int) -> "OrderBuilder":
        """Set the order ID."""
        self._id = order_id
        return self

    def with_customer(self, customer_id: int) -> "OrderBuilder":
        """Set the customer ID."""
        self._customer_id = customer_id
        return self

    def with_items(self, items: list[str]) -> "OrderBuilder":
        """Set the order items."""
        self._items = items
        return self

    def with_total(self, total: float) -> "OrderBuilder":
        """Set the order total."""
        self._total = total
        return self

    def with_status(self, status: str) -> "OrderBuilder":
        """Set the order status."""
        self._status = status
        return self

    def completed(self) -> "OrderBuilder":
        """Configure as a completed order."""
        self._status = "completed"
        return self

    def build(self) -> Order:
        """Build and return the Order instance."""
        return Order(
            id=self._id,
            customer_id=self._customer_id,
            items=self._items,
            total=self._total,
            status=self._status,
        )

Usage

# Test Fixture usage
with database_fixture() as ctx:
    assert ctx.data["db"]["connected"] is True
    # Run tests with database context
# After context exits, connection is closed

# Data Fixture usage
admin = UserFixtures.admin_user()
assert admin.role == "admin"

all_users = UserFixtures.users_list()
assert len(all_users) == 3

# Test Data Builder usage
order = (
    OrderBuilder()
    .with_customer(42)
    .with_items(["widget", "gadget"])
    .with_total(99.99)
    .completed()
    .build()
)
assert order.status == "completed"
assert order.customer_id == 42

Trade-offs

Benefits:

  1. Reduces test code duplication through reusable setup logic
  2. Improves test readability by separating setup from assertions
  3. Ensures consistent test data across the test suite
  4. Test Data Builder allows creating complex objects with minimal boilerplate

Drawbacks:

  1. Adds indirection that may obscure what data a test uses
  2. Shared fixtures can create hidden dependencies between tests
  3. Overly generic fixtures may not fit specific test requirements
  4. Fixture maintenance overhead increases as the test suite grows

Real-World Examples

  • pytest fixtures for database connections and mock services
  • Django TestCase with setUp() and tearDown() methods
  • Factory libraries (factory_boy, Faker) for generating test data
  • Database seeding scripts for integration tests
  • Factory (fixtures often use factories internally)
  • Builder (Test Data Builder is a specialized application)
  • Singleton (shared fixtures may use singleton semantics)
  • Template Method (fixture lifecycle follows template pattern)

API Reference

design_patterns.testing.fixture

Fixture pattern implementations for testing.

This module provides three complementary approaches to test fixtures:

  1. FixtureContext / context manager fixtures: Manage setup and teardown of test state
  2. Data Fixtures: Provide predefined test data through static factory methods
  3. Test Data Builder: Create complex test objects with fluent interface

These patterns help reduce test code duplication, improve readability, and ensure consistent test data across a test suite.

FixtureContext dataclass

Container for test state managed by a fixture.

Attributes:

Name Type Description
data dict[str, Any]

Dictionary holding arbitrary test data and state.

resources list[Any]

List of resources that need cleanup after the test.

Example
ctx = FixtureContext()
ctx.data["connection"] = create_connection()
ctx.resources.append(ctx.data["connection"])
Source code in src/design_patterns/testing/fixture.py
@dataclass
class FixtureContext:
    """Container for test state managed by a fixture.

    Attributes:
        data: Dictionary holding arbitrary test data and state.
        resources: List of resources that need cleanup after the test.

    Example:
        ```python
        ctx = FixtureContext()
        ctx.data["connection"] = create_connection()
        ctx.resources.append(ctx.data["connection"])
        ```
    """

    data: dict[str, Any] = field(default_factory=dict)
    resources: list[Any] = field(default_factory=list)

Order dataclass

Domain object representing an order.

Attributes:

Name Type Description
id int

Unique order identifier.

customer_id int

ID of the customer who placed the order.

items list[str]

List of item names in the order.

total float

Total price of the order.

status str

Current order status.

Source code in src/design_patterns/testing/fixture.py
@dataclass
class Order:
    """Domain object representing an order.

    Attributes:
        id: Unique order identifier.
        customer_id: ID of the customer who placed the order.
        items: List of item names in the order.
        total: Total price of the order.
        status: Current order status.
    """

    id: int
    customer_id: int
    items: list[str]
    total: float
    status: str

OrderBuilder

Fluent builder for creating Order test objects with sensible defaults.

Implements the Test Data Builder pattern, allowing tests to specify only the attributes relevant to the test while using reasonable defaults for everything else.

Example
order = (
    OrderBuilder()
    .with_customer(42)
    .with_items(["widget", "gadget"])
    .completed()
    .build()
)
assert order.customer_id == 42
assert order.status == "completed"
Source code in src/design_patterns/testing/fixture.py
class OrderBuilder:
    """Fluent builder for creating Order test objects with sensible defaults.

    Implements the Test Data Builder pattern, allowing tests to specify only
    the attributes relevant to the test while using reasonable defaults
    for everything else.

    Example:
        ```python
        order = (
            OrderBuilder()
            .with_customer(42)
            .with_items(["widget", "gadget"])
            .completed()
            .build()
        )
        assert order.customer_id == 42
        assert order.status == "completed"
        ```
    """

    def __init__(self) -> None:
        """Initialize builder with default values."""
        self._id: int = 1
        self._customer_id: int = 100
        self._items: list[str] = ["default_item"]
        self._total: float = 0.0
        self._status: str = "pending"

    def with_id(self, order_id: int) -> "OrderBuilder":
        """Set the order ID.

        Args:
            order_id: The order identifier.

        Returns:
            OrderBuilder: Self for method chaining.
        """
        self._id = order_id
        return self

    def with_customer(self, customer_id: int) -> "OrderBuilder":
        """Set the customer ID.

        Args:
            customer_id: The customer identifier.

        Returns:
            OrderBuilder: Self for method chaining.
        """
        self._customer_id = customer_id
        return self

    def with_items(self, items: list[str]) -> "OrderBuilder":
        """Set the order items.

        Args:
            items: List of item names.

        Returns:
            OrderBuilder: Self for method chaining.
        """
        self._items = items
        return self

    def with_total(self, total: float) -> "OrderBuilder":
        """Set the order total.

        Args:
            total: The total price.

        Returns:
            OrderBuilder: Self for method chaining.
        """
        self._total = total
        return self

    def with_status(self, status: str) -> "OrderBuilder":
        """Set the order status.

        Args:
            status: The order status string.

        Returns:
            OrderBuilder: Self for method chaining.
        """
        self._status = status
        return self

    def completed(self) -> "OrderBuilder":
        """Configure as a completed order.

        Returns:
            OrderBuilder: Self for method chaining.
        """
        self._status = "completed"
        return self

    def build(self) -> Order:
        """Build and return the Order instance.

        Returns:
            Order: The constructed Order object.
        """
        return Order(
            id=self._id,
            customer_id=self._customer_id,
            items=self._items,
            total=self._total,
            status=self._status,
        )

__init__()

Initialize builder with default values.

Source code in src/design_patterns/testing/fixture.py
def __init__(self) -> None:
    """Initialize builder with default values."""
    self._id: int = 1
    self._customer_id: int = 100
    self._items: list[str] = ["default_item"]
    self._total: float = 0.0
    self._status: str = "pending"

build()

Build and return the Order instance.

Returns:

Name Type Description
Order Order

The constructed Order object.

Source code in src/design_patterns/testing/fixture.py
def build(self) -> Order:
    """Build and return the Order instance.

    Returns:
        Order: The constructed Order object.
    """
    return Order(
        id=self._id,
        customer_id=self._customer_id,
        items=self._items,
        total=self._total,
        status=self._status,
    )

completed()

Configure as a completed order.

Returns:

Name Type Description
OrderBuilder OrderBuilder

Self for method chaining.

Source code in src/design_patterns/testing/fixture.py
def completed(self) -> "OrderBuilder":
    """Configure as a completed order.

    Returns:
        OrderBuilder: Self for method chaining.
    """
    self._status = "completed"
    return self

with_customer(customer_id)

Set the customer ID.

Parameters:

Name Type Description Default
customer_id int

The customer identifier.

required

Returns:

Name Type Description
OrderBuilder OrderBuilder

Self for method chaining.

Source code in src/design_patterns/testing/fixture.py
def with_customer(self, customer_id: int) -> "OrderBuilder":
    """Set the customer ID.

    Args:
        customer_id: The customer identifier.

    Returns:
        OrderBuilder: Self for method chaining.
    """
    self._customer_id = customer_id
    return self

with_id(order_id)

Set the order ID.

Parameters:

Name Type Description Default
order_id int

The order identifier.

required

Returns:

Name Type Description
OrderBuilder OrderBuilder

Self for method chaining.

Source code in src/design_patterns/testing/fixture.py
def with_id(self, order_id: int) -> "OrderBuilder":
    """Set the order ID.

    Args:
        order_id: The order identifier.

    Returns:
        OrderBuilder: Self for method chaining.
    """
    self._id = order_id
    return self

with_items(items)

Set the order items.

Parameters:

Name Type Description Default
items list[str]

List of item names.

required

Returns:

Name Type Description
OrderBuilder OrderBuilder

Self for method chaining.

Source code in src/design_patterns/testing/fixture.py
def with_items(self, items: list[str]) -> "OrderBuilder":
    """Set the order items.

    Args:
        items: List of item names.

    Returns:
        OrderBuilder: Self for method chaining.
    """
    self._items = items
    return self

with_status(status)

Set the order status.

Parameters:

Name Type Description Default
status str

The order status string.

required

Returns:

Name Type Description
OrderBuilder OrderBuilder

Self for method chaining.

Source code in src/design_patterns/testing/fixture.py
def with_status(self, status: str) -> "OrderBuilder":
    """Set the order status.

    Args:
        status: The order status string.

    Returns:
        OrderBuilder: Self for method chaining.
    """
    self._status = status
    return self

with_total(total)

Set the order total.

Parameters:

Name Type Description Default
total float

The total price.

required

Returns:

Name Type Description
OrderBuilder OrderBuilder

Self for method chaining.

Source code in src/design_patterns/testing/fixture.py
def with_total(self, total: float) -> "OrderBuilder":
    """Set the order total.

    Args:
        total: The total price.

    Returns:
        OrderBuilder: Self for method chaining.
    """
    self._total = total
    return self

User dataclass

Domain object representing a user.

Attributes:

Name Type Description
id int

Unique user identifier.

name str

User's display name.

email str

User's email address.

role str

User's role (defaults to "user").

Source code in src/design_patterns/testing/fixture.py
@dataclass
class User:
    """Domain object representing a user.

    Attributes:
        id: Unique user identifier.
        name: User's display name.
        email: User's email address.
        role: User's role (defaults to "user").
    """

    id: int
    name: str
    email: str
    role: str = "user"

UserFixtures

Predefined user data for testing.

Provides static factory methods that return User instances with predetermined values, ensuring consistent test data.

Example
admin = UserFixtures.admin_user()
assert admin.role == "admin"
Source code in src/design_patterns/testing/fixture.py
class UserFixtures:
    """Predefined user data for testing.

    Provides static factory methods that return User instances with
    predetermined values, ensuring consistent test data.

    Example:
        ```python
        admin = UserFixtures.admin_user()
        assert admin.role == "admin"
        ```
    """

    @staticmethod
    def admin_user() -> User:
        """Returns a standard admin user for testing.

        Returns:
            User: Admin user with id=1, role="admin".
        """
        return User(id=1, name="Admin", email="admin@example.com", role="admin")

    @staticmethod
    def regular_user() -> User:
        """Returns a standard regular user for testing.

        Returns:
            User: Regular user with id=2, role="user".
        """
        return User(id=2, name="John Doe", email="john@example.com", role="user")

    @staticmethod
    def users_list() -> list[User]:
        """Returns a list of test users.

        Returns:
            list[User]: List containing admin, regular user, and additional user.
        """
        return [
            UserFixtures.admin_user(),
            UserFixtures.regular_user(),
            User(id=3, name="Jane Doe", email="jane@example.com"),
        ]

admin_user() staticmethod

Returns a standard admin user for testing.

Returns:

Name Type Description
User User

Admin user with id=1, role="admin".

Source code in src/design_patterns/testing/fixture.py
@staticmethod
def admin_user() -> User:
    """Returns a standard admin user for testing.

    Returns:
        User: Admin user with id=1, role="admin".
    """
    return User(id=1, name="Admin", email="admin@example.com", role="admin")

regular_user() staticmethod

Returns a standard regular user for testing.

Returns:

Name Type Description
User User

Regular user with id=2, role="user".

Source code in src/design_patterns/testing/fixture.py
@staticmethod
def regular_user() -> User:
    """Returns a standard regular user for testing.

    Returns:
        User: Regular user with id=2, role="user".
    """
    return User(id=2, name="John Doe", email="john@example.com", role="user")

users_list() staticmethod

Returns a list of test users.

Returns:

Type Description
list[User]

list[User]: List containing admin, regular user, and additional user.

Source code in src/design_patterns/testing/fixture.py
@staticmethod
def users_list() -> list[User]:
    """Returns a list of test users.

    Returns:
        list[User]: List containing admin, regular user, and additional user.
    """
    return [
        UserFixtures.admin_user(),
        UserFixtures.regular_user(),
        User(id=3, name="Jane Doe", email="jane@example.com"),
    ]

database_fixture()

Context manager fixture that sets up and tears down a test database connection.

Yields:

Name Type Description
FixtureContext FixtureContext

Context containing simulated database connection state.

Example
with database_fixture() as ctx:
    assert ctx.data["db"]["connected"] is True
    # Run database tests here
# Connection automatically closed after context exits
Source code in src/design_patterns/testing/fixture.py
@contextmanager
def database_fixture() -> Generator[FixtureContext, None, None]:
    """Context manager fixture that sets up and tears down a test database connection.

    Yields:
        FixtureContext: Context containing simulated database connection state.

    Example:
        ```python
        with database_fixture() as ctx:
            assert ctx.data["db"]["connected"] is True
            # Run database tests here
        # Connection automatically closed after context exits
        ```
    """
    context = FixtureContext()
    # Setup: create test database connection
    context.data["db"] = {"connected": True, "tables": []}
    context.resources.append("db_connection")

    try:
        yield context
    finally:
        # Teardown: close connection and cleanup
        context.data["db"]["connected"] = False
        context.resources.clear()