Python · Testing with pytest · Advanced
Fixture Factories and Test Data
Build flexible test data using factory fixtures and the Faker library.
Quick topic start and explanations before exercises (exercises below):
Factory fixtures in practice: users, products, and Faker
#Fixture factories and Faker reference card
#Exercises:
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']
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)
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)
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
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