Python · Testing with pytest · Advanced

Fixture Factories and Test Data

5 tasks

Build flexible test data using factory fixtures and the Faker library.

The factory pattern: fixtures that create data on demand

#
## The limitation of fixed-value fixtures A regular fixture returns a single, fixed value. Every test that requests it gets the same object: ```python @pytest.fixture def user(): return {'username': 'alice', 'email': '[email protected]', 'role': 'admin'} ``` This works fine when one test needs one user. It breaks down as soon as you need variation: ```python def test_duplicate_username(user): user_a = user # {'username': 'alice', ...} user_b = user # same dict — no way to get a different user assert user_a['username'] != user_b['username'] # FAILS: 'alice' == 'alice' ``` You could write separate fixtures (`user_alice`, `user_bob`, `user_guest`), but this creates fixture proliferation — dozens of nearly identical fixtures for every combination of fields. ## The factory pattern: a fixture that returns a callable Instead of returning a value, the fixture returns a *factory function*. Tests call the factory with arguments to create as many customised objects as they need: ```python @pytest.fixture def make_user(): def _factory(username='alice', email=None, role='user'): if email is None: email = f'{username}@example.com' return {'username': username, 'email': email, 'role': role} return _factory ``` ```python def test_duplicate_username(make_user): user_a = make_user(username='alice') user_b = make_user(username='bob') assert user_a['username'] != user_b['username'] # PASSES ``` The fixture (`make_user`) runs once and returns the factory function. Each call to `make_user(...)` inside the test creates a fresh dict with the requested fields, using sensible defaults for anything not specified. ## Why defaults matter Good factory defaults let test code express only the *relevant* variation: ```python def test_admin_can_delete(make_user): admin = make_user(role='admin') # only the role matters here assert can_delete(admin) is True def test_guest_cannot_delete(make_user): guest = make_user(role='guest') assert can_delete(guest) is False ``` The `username` and `email` fields are irrelevant to these tests. With defaults, you don't have to specify them. If you needed 50 test cases, each with a different role, you would not write 50 fixtures — you would parametrize the factory call. ## Faker: realistic test data The `faker` library generates realistic fake data — names, emails, addresses, phone numbers, lorem ipsum text: ```bash pip install faker ``` ```python from faker import Faker fake = Faker() print(fake.name()) # 'Emily Rodriguez' print(fake.email()) # '[email protected]' print(fake.user_name()) # 'jsmith42' print(fake.pyint(min_value=1, max_value=1000)) # 847 ``` Integrating Faker into a factory fixture gives you unique, realistic values by default: ```python from faker import Faker import pytest @pytest.fixture def make_user(): fake = Faker() def _factory(**kwargs): defaults = { 'username': fake.user_name(), 'email': fake.email(), 'first_name': fake.first_name(), 'last_name': fake.last_name(), } defaults.update(kwargs) return defaults return _factory ``` Now every call to `make_user()` produces a unique user with a different generated username and email, avoiding accidental data collisions between tests. ## Reproducibility: seeding Faker Faker's output is random by default. If a test fails, reproducing the failure requires the same data. Seed Faker at the start of your test session for reproducible output: ```python @pytest.fixture(scope='session', autouse=True) def seed_faker(): Faker.seed(42) # same seed → same sequence of fake data on every run ``` With a fixed seed, the sequence of generated values is deterministic: the 3rd `make_user()` call in your test suite always produces the same username. Without a seed, random data in tests can cause intermittent failures that are hard to reproduce. ## When to use factories vs plain fixtures | Situation | Use | |---|---| | Every test needs the same object | Plain `@pytest.fixture` | | Tests need objects with slight variations | Factory fixture | | Tests create multiple objects of the same type | Factory fixture | | Complex nested structures with many defaults | Factory fixture | | Simple scalar value (URL, config dict, number) | Plain `@pytest.fixture` | The factory pattern adds a layer of indirection. Use it when that indirection pays off — when tests genuinely need different instances, not just different references to the same one. ## Composing factories Factories can call other factories to build nested structures: ```python @pytest.fixture def make_order(make_user, make_product): def _factory(num_items=1, **kwargs): defaults = { 'user': make_user(), 'items': [make_product() for _ in range(num_items)], 'status': 'pending', } defaults.update(kwargs) return defaults return _factory ``` ```python def test_order_total(make_order, make_product): product = make_product(price=10.00) order = make_order(items=[product, product]) assert calculate_total(order) == 20.00 ``` The `make_order` fixture depends on `make_user` and `make_product` — standard fixture chaining. pytest resolves the dependency graph automatically.

Factory fixtures in practice: users, products, and Faker

#
## Project setup ```bash pip install faker pytest ``` ## Example 1: A basic make_user factory ```python # conftest.py import pytest @pytest.fixture def make_user(): created = [] def _factory(username='testuser', email=None, role='user', age=25): if email is None: email = f'{username}@example.com' user = {'username': username, 'email': email, 'role': role, 'age': age} created.append(user) return user return _factory ``` The factory tracks all created users in `created` — useful if you need to clean them up in teardown (e.g., deleting them from a database after the test). For plain dicts there is no teardown needed, but the tracking pattern becomes essential when factories create real database rows or API resources. ```python # test_users.py def test_user_defaults(make_user): user = make_user() assert user['username'] == 'testuser' assert user['email'] == '[email protected]' assert user['role'] == 'user' def test_user_custom_role(make_user): admin = make_user(username='admin', role='admin') assert admin['role'] == 'admin' def test_two_users_in_one_test(make_user): buyer = make_user(username='buyer') seller = make_user(username='seller') assert buyer['username'] != seller['username'] ``` ## Example 2: Integrating Faker for realistic data ```python # conftest.py import pytest from faker import Faker @pytest.fixture(scope='session', autouse=True) def seed_faker(): Faker.seed(0) # reproducible across runs @pytest.fixture def make_user(): fake = Faker() def _factory(**kwargs): username = fake.user_name() defaults = { 'username': username, 'email': fake.email(), 'first_name': fake.first_name(), 'last_name': fake.last_name(), 'bio': fake.text(max_nb_chars=100), } defaults.update(kwargs) return defaults return _factory ``` ```python # test_faker_users.py def test_users_have_unique_usernames(make_user): users = [make_user() for _ in range(5)] usernames = [u['username'] for u in users] assert len(set(usernames)) == len(usernames) # all unique def test_email_override(make_user): user = make_user(email='[email protected]') assert user['email'] == '[email protected]' assert 'username' in user # other fields still generated by Faker ``` The `autouse=True` session fixture seeds Faker once at the start of the run. Every subsequent `fake.user_name()` call produces the same sequence on every run — critical for reproducing test failures. ## Example 3: make_product factory ```python # conftest.py (continued) @pytest.fixture def make_product(): fake = Faker() def _factory(**kwargs): defaults = { 'name': fake.catch_phrase(), 'slug': fake.slug(), 'price': round(fake.pyfloat(min_value=1, max_value=999, right_digits=2), 2), 'in_stock': True, } defaults.update(kwargs) return defaults return _factory ``` ```python # test_products.py def test_three_products(make_product): products = [ make_product(name='Widget A', price=9.99), make_product(name='Widget B', price=19.99), make_product(name='Widget C', price=4.99), ] assert len(products) == 3 assert all(p['in_stock'] for p in products) total = sum(p['price'] for p in products) assert round(total, 2) == 34.97 def test_out_of_stock(make_product): product = make_product(in_stock=False) assert product['in_stock'] is False ``` ## Example 4: Factory combined with parametrize ```python # test_roles.py import pytest @pytest.mark.parametrize('role,can_admin', [ ('admin', True), ('editor', False), ('viewer', False), ]) def test_admin_permission(make_user, role, can_admin): user = make_user(role=role) assert check_admin_permission(user) is can_admin def check_admin_permission(user): return user['role'] == 'admin' ``` `parametrize` supplies the variation; the factory supplies the object. This combination is more readable than a fixture that tries to embed all variations internally. ## Example 5: Nested factory (order with line items) ```python # conftest.py (continued) @pytest.fixture def make_order(make_user, make_product): def _factory(num_items=1, **kwargs): defaults = { 'user': make_user(), 'items': [make_product() for _ in range(num_items)], 'status': 'pending', 'discount': 0.0, } defaults.update(kwargs) return defaults return _factory ``` ```python # test_orders.py def test_order_item_count(make_order): order = make_order(num_items=3) assert len(order['items']) == 3 def test_order_with_custom_item(make_order, make_product): expensive = make_product(price=999.99) order = make_order(items=[expensive]) assert order['items'][0]['price'] == 999.99 ```

Fixture factories and Faker reference card

#
## Factory fixture template ```python import pytest from faker import Faker @pytest.fixture(scope='session', autouse=True) def seed_faker(): Faker.seed(0) # remove for random data per run @pytest.fixture def make_<model>(): fake = Faker() def _factory(**kwargs): defaults = { 'field1': fake.some_provider(), 'field2': 'sensible_default', } defaults.update(kwargs) # caller overrides win return defaults return _factory ``` ## Faker: commonly used providers | Provider | Example output | |---|---| | `fake.name()` | `'Emily Rodriguez'` | | `fake.first_name()` | `'Emily'` | | `fake.last_name()` | `'Rodriguez'` | | `fake.user_name()` | `'emily_r42'` | | `fake.email()` | `'[email protected]'` | | `fake.slug()` | `'fast-blue-widget'` | | `fake.text(max_nb_chars=100)` | lorem ipsum sentence | | `fake.url()` | `'https://example.com/path'` | | `fake.pyfloat(min_value=1, max_value=100, right_digits=2)` | `47.83` | | `fake.pyint(min_value=1, max_value=1000)` | `342` | | `fake.boolean()` | `True` / `False` | | `fake.uuid4()` | `'3d...a1'` | ## Patterns ```python # One object user = make_user() # With overrides admin = make_user(role='admin') # Multiple objects products = [make_product() for _ in range(5)] # Nested order = make_order(num_items=3) # Combined with parametrize @pytest.mark.parametrize('role', ['admin', 'editor', 'viewer']) def test_role(make_user, role): user = make_user(role=role) ... ``` ## When to use each approach | Need | Solution | |---|---| | One fixed object for all tests | Plain `@pytest.fixture` | | Multiple objects with variations | Factory fixture | | Realistic-looking data | Factory + Faker | | Reproducible random data | `Faker.seed(n)` in autouse session fixture | | Nested structures | Compose factories (factory calls other factory) |
01

Write a make_user factory fixture

#

Write a pytest fixture `make_user` that returns a factory function. The factory should: - Accept keyword arguments: `username`, `email`, `role`, `age` - Use sensible defaults: `username='testuser'`, `email` derived from username if not given, `role='user'`, `age=25` - Return a dict with all four fields Write two tests that use it: 1. `test_default_user` — create a user with no arguments, assert `role == 'user'` and email contains the username 2. `test_two_users` — create two users with different usernames, assert they are different

# test_factory_basic.py
import pytest


@pytest.fixture
def make_user():
    def _factory(username='testuser', email=None, role='user', age=25):
        # if email is None, derive it from username
        ...
        return ...
    return _factory


def test_default_user(make_user):
    user = make_user()
    assert user['role'] == 'user'
    assert ...  # email contains username


def test_two_users(make_user):
    a = make_user(username='alice')
    b = make_user(username='bob')
    assert ...
Solution
# test_factory_basic.py
import pytest


@pytest.fixture
def make_user():
    def _factory(username='testuser', email=None, role='user', age=25):
        if email is None:
            email = f'{username}@example.com'
        return {'username': username, 'email': email, 'role': role, 'age': age}
    return _factory


def test_default_user(make_user):
    user = make_user()
    assert user['role'] == 'user'
    assert user['username'] in user['email']


def test_two_users(make_user):
    a = make_user(username='alice')
    b = make_user(username='bob')
    assert a['username'] != b['username']
02

Add Faker for realistic generated data

#

Install `faker` and update `make_user` to generate realistic fake data by default: - `username` from `fake.user_name()` - `email` from `fake.email()` - `first_name` and `last_name` from `fake.first_name()` / `fake.last_name()` All fields should still be overridable with keyword arguments. Add a session-scoped `autouse` fixture that calls `Faker.seed(0)` for reproducible output. Write a test `test_users_have_unique_emails` that creates 5 users with no arguments and asserts all their emails are distinct.

# test_faker_factory.py
import pytest
from faker import Faker


@pytest.fixture(scope='session', autouse=True)
def seed_faker():
    Faker.seed(0)


@pytest.fixture
def make_user():
    fake = Faker()

    def _factory(**kwargs):
        defaults = {
            'username': ...,
            'email': ...,
            'first_name': ...,
            'last_name': ...,
        }
        defaults.update(kwargs)
        return defaults

    return _factory


def test_users_have_unique_emails(make_user):
    users = [make_user() for _ in range(5)]
    emails = [u['email'] for u in users]
    assert len(set(emails)) == len(emails)
Solution
# test_faker_factory.py
import pytest
from faker import Faker


@pytest.fixture(scope='session', autouse=True)
def seed_faker():
    Faker.seed(0)


@pytest.fixture
def make_user():
    fake = Faker()

    def _factory(**kwargs):
        defaults = {
            'username': fake.user_name(),
            'email': fake.email(),
            'first_name': fake.first_name(),
            'last_name': fake.last_name(),
        }
        defaults.update(kwargs)
        return defaults

    return _factory


def test_users_have_unique_emails(make_user):
    users = [make_user() for _ in range(5)]
    emails = [u['email'] for u in users]
    assert len(set(emails)) == len(emails)
03

Write a make_product factory and create multiple products

#

Write a `make_product` factory fixture. Each product should have: - `name` — a string (use Faker or a simple default like `'Product'`) - `slug` — derived from name if not provided (lowercase, spaces → hyphens) - `price` — a float, default `9.99` - `in_stock` — bool, default `True` Write `test_three_products(make_product)` that: - Creates 3 products: prices `5.00`, `10.00`, `20.00` - Asserts all three are in stock - Asserts the sum of their prices equals `35.00`

# test_products_factory.py
import pytest


@pytest.fixture
def make_product():
    def _factory(name='Test Product', slug=None, price=9.99, in_stock=True):
        if slug is None:
            slug = name.lower().replace(' ', '-')
        return {'name': name, 'slug': slug, 'price': price, 'in_stock': in_stock}
    return _factory


def test_three_products(make_product):
    products = [
        make_product(price=...),
        make_product(price=...),
        make_product(price=...),
    ]
    assert all(...)
    assert sum(p['price'] for p in products) == ...
Solution
# test_products_factory.py
import pytest


@pytest.fixture
def make_product():
    def _factory(name='Test Product', slug=None, price=9.99, in_stock=True):
        if slug is None:
            slug = name.lower().replace(' ', '-')
        return {'name': name, 'slug': slug, 'price': price, 'in_stock': in_stock}
    return _factory


def test_three_products(make_product):
    products = [
        make_product(price=5.00),
        make_product(price=10.00),
        make_product(price=20.00),
    ]
    assert all(p['in_stock'] for p in products)
    assert sum(p['price'] for p in products) == pytest.approx(35.00)
04

Write a factory for a nested structure

#

Write a `make_order` factory fixture that depends on `make_user` and `make_product`. An order should be a dict with: - `user` — a user dict (from `make_user()`) - `items` — a list of product dicts (default: one product from `make_product()`) - `status` — string, default `'pending'` Write two tests: 1. `test_order_default_has_one_item` — create an order with defaults, assert `len(order['items']) == 1` 2. `test_order_custom_items` — create two products explicitly, pass them as `items`, assert the order has 2 items

# test_order_factory.py
import pytest


@pytest.fixture
def make_user():
    def _factory(**kwargs):
        defaults = {'username': 'testuser', 'email': '[email protected]', 'role': 'user'}
        defaults.update(kwargs)
        return defaults
    return _factory


@pytest.fixture
def make_product():
    def _factory(**kwargs):
        defaults = {'name': 'Widget', 'price': 9.99, 'in_stock': True}
        defaults.update(kwargs)
        return defaults
    return _factory


@pytest.fixture
def make_order(make_user, make_product):
    def _factory(**kwargs):
        defaults = {
            'user': ...,
            'items': [...],
            'status': 'pending',
        }
        defaults.update(kwargs)
        return defaults
    return _factory


def test_order_default_has_one_item(make_order):
    order = make_order()
    assert len(order['items']) == 1


def test_order_custom_items(make_order, make_product):
    p1 = make_product(name='A')
    p2 = make_product(name='B')
    order = make_order(items=[p1, p2])
    assert len(order['items']) == 2
Solution
# test_order_factory.py
import pytest


@pytest.fixture
def make_user():
    def _factory(**kwargs):
        defaults = {'username': 'testuser', 'email': '[email protected]', 'role': 'user'}
        defaults.update(kwargs)
        return defaults
    return _factory


@pytest.fixture
def make_product():
    def _factory(**kwargs):
        defaults = {'name': 'Widget', 'price': 9.99, 'in_stock': True}
        defaults.update(kwargs)
        return defaults
    return _factory


@pytest.fixture
def make_order(make_user, make_product):
    def _factory(**kwargs):
        defaults = {
            'user': make_user(),
            'items': [make_product()],
            'status': 'pending',
        }
        defaults.update(kwargs)
        return defaults
    return _factory


def test_order_default_has_one_item(make_order):
    order = make_order()
    assert len(order['items']) == 1


def test_order_custom_items(make_order, make_product):
    p1 = make_product(name='A')
    p2 = make_product(name='B')
    order = make_order(items=[p1, p2])
    assert len(order['items']) == 2
05

Combine a factory fixture with parametrize

#

Using the `make_user` factory fixture, write a parametrized test that checks a `has_access(user, resource)` function. Implement `has_access(user, resource)`: - Returns `True` if `user['role'] == 'admin'` or `resource == 'public'` - Returns `False` otherwise Parametrize `test_access` with these cases (role, resource, expected): - `('admin', 'secret', True)` - `('user', 'public', True)` - `('user', 'secret', False)` - `('guest', 'private', False)` Use `make_user(role=role)` inside the test body to create the user.

# test_access.py
import pytest


def has_access(user, resource):
    ...


@pytest.fixture
def make_user():
    def _factory(**kwargs):
        defaults = {'username': 'testuser', 'role': 'user'}
        defaults.update(kwargs)
        return defaults
    return _factory


@pytest.mark.parametrize('role,resource,expected', [
    ('admin', 'secret', True),
    ('user',  'public', True),
    ('user',  'secret', False),
    ('guest', 'private', False),
])
def test_access(make_user, role, resource, expected):
    user = make_user(role=role)
    assert has_access(user, resource) is expected
Solution
# test_access.py
import pytest


def has_access(user, resource):
    return user['role'] == 'admin' or resource == 'public'


@pytest.fixture
def make_user():
    def _factory(**kwargs):
        defaults = {'username': 'testuser', 'role': 'user'}
        defaults.update(kwargs)
        return defaults
    return _factory


@pytest.mark.parametrize('role,resource,expected', [
    ('admin', 'secret', True),
    ('user',  'public', True),
    ('user',  'secret', False),
    ('guest', 'private', False),
])
def test_access(make_user, role, resource, expected):
    user = make_user(role=role)
    assert has_access(user, resource) is expected