Fixture factories и indirect parametrization
Нужно протестировать функцию с 5 разными пользователями: admin, moderator, user, guest, banned. Создавать 5 фикстур? Есть способ лучше!
Цель: Освоить fixture factories и indirect parametrization для гибкого создания тестовых данных.
Вы точно готовы?
Убедитесь, что умеете:
# Параметризация тестов
@pytest.mark.parametrize("value", [1, 2, 3])
def test_values(value):
assert value > 0
# Фикстуры с scope
@pytest.fixture(scope="module")
def database():
return connect_to_db()Если parametrize или scope непонятны — вернитесь к предыдущим урокам.
Проблема: множество похожих фикстур
Дублирование фикстур
# ❌ ПЛОХО — 5 почти одинаковых фикстур
@pytest.fixture
def admin_user():
return User(name="Admin", role="admin", permissions=["all"])
@pytest.fixture
def moderator_user():
return User(name="Moderator", role="moderator", permissions=["moderate"])
@pytest.fixture
def regular_user():
return User(name="User", role="user", permissions=["read", "write"])
@pytest.fixture
def guest_user():
return User(name="Guest", role="guest", permissions=["read"])
@pytest.fixture
def banned_user():
return User(name="Banned", role="banned", permissions=[])Проблемы:
- ❌ Дублирование кода
- ❌ Сложно добавить новые роли
- ❌ Сложно изменить структуру User
Решение 1: Fixture factory
Фикстура возвращает функцию
@pytest.fixture
def make_user():
"""Factory для создания пользователей"""
def _make_user(role="user", name=None, permissions=None):
if name is None:
name = f"{role.capitalize()}User"
if permissions is None:
permissions = {
"admin": ["all"],
"moderator": ["read", "write", "moderate"],
"user": ["read", "write"],
"guest": ["read"],
"banned": []
}[role]
return User(name=name, role=role, permissions=permissions)
return _make_userИспользование:
def test_admin_can_delete(make_user):
admin = make_user(role="admin")
assert admin.can("delete")
def test_guest_cannot_write(make_user):
guest = make_user(role="guest")
assert not guest.can("write")
def test_custom_user(make_user):
custom = make_user(role="custom", permissions=["special"])
assert custom.can("special")✅ Одна фикстура, любые вариации!
Factory с cleanup
@pytest.fixture
def make_database_user(database):
"""Factory для пользователей в БД"""
created_users = []
def _make_user(**kwargs):
user = User.create(database, **kwargs)
created_users.append(user)
return user
yield _make_user
# Cleanup: удаляем всех созданных пользователей
for user in created_users:
user.delete()Использование:
def test_multiple_users(make_database_user):
alice = make_database_user(name="Alice", age=25)
bob = make_database_user(name="Bob", age=30)
charlie = make_database_user(name="Charlie", age=35)
assert User.count() == 3
# После теста все 3 пользователя удалятся автоматически!Практический пример: order factory
@pytest.fixture
def make_order(database):
"""Factory для заказов"""
orders = []
def _make_order(user_id=1, items=None, status="pending"):
if items is None:
items = [{"product": "Laptop", "qty": 1}]
order = Order.create(
database,
user_id=user_id,
items=items,
status=status,
total=sum(item["qty"] * 100 for item in items)
)
orders.append(order)
return order
yield _make_order
for order in orders:
order.delete()
def test_order_total(make_order):
order = make_order(items=[
{"product": "Laptop", "qty": 2},
{"product": "Mouse", "qty": 5},
])
assert order.total == 700
def test_order_status_flow(make_order):
order = make_order(status="pending")
order.process()
assert order.status == "processing"Решение 2: Indirect parametrization
Проблема: параметризация фикстур
# Хочу запустить тест с разными пользователями
@pytest.mark.parametrize("user", ["admin", "moderator", "user"])
def test_access(user):
# user — это строка "admin", не объект User!
assert user.can("read") # ❌ Не работаетРешение: indirect=True
@pytest.fixture
def user(request):
"""Создаёт пользователя по роли из параметра"""
role = request.param # Получаем параметр из parametrize
return User(name=role.capitalize(), role=role)
@pytest.mark.parametrize("user", ["admin", "moderator", "user"], indirect=True)
def test_access(user):
# user — это объект User!
assert user.can("read") # ✅ РаботаетЧто происходит:
@pytest.mark.parametrize("user", ["admin", ...], indirect=True)- pytest передаёт
"admin"в фикстуруuserчерезrequest.param - Фикстура создаёт
User(role="admin") - Тест получает объект
User
✅ Тест запустится 3 раза с разными пользователями!
Множественная indirect параметризация
@pytest.fixture
def user(request):
role = request.param
return User(role=role)
@pytest.fixture
def database(request):
db_type = request.param
return connect_to_db(db_type)
@pytest.mark.parametrize("user", ["admin", "user"], indirect=True)
@pytest.mark.parametrize("database", ["postgres", "mysql"], indirect=True)
def test_user_database(user, database):
"""Тест комбинации user × database"""
user.save(database)
assert user.id is not NoneРезультат: 4 теста (2 пользователя × 2 БД)
test_user_database[postgres-admin] PASSED
test_user_database[postgres-user] PASSED
test_user_database[mysql-admin] PASSED
test_user_database[mysql-user] PASSEDПрактический пример: тестирование permissions
@pytest.fixture
def user(request):
"""Фикстура создаёт пользователя по роли"""
role = request.param
permissions = {
"admin": ["read", "write", "delete", "admin"],
"moderator": ["read", "write", "moderate"],
"user": ["read", "write"],
"guest": ["read"],
"banned": []
}
return User(role=role, permissions=permissions[role])
@pytest.mark.parametrize("user", ["admin", "moderator"], indirect=True)
def test_can_moderate(user):
"""Админ и модератор могут модерировать"""
assert user.can("moderate") or user.role == "admin"
@pytest.mark.parametrize("user", ["user", "guest", "banned"], indirect=True)
def test_cannot_moderate(user):
"""User, guest и banned не могут модерировать"""
assert not user.can("moderate")
@pytest.mark.parametrize("user", ["admin", "moderator", "user", "guest", "banned"], indirect=True)
def test_everyone_can_read(user):
"""Все кроме banned могут читать"""
if user.role != "banned":
assert user.can("read")
else:
assert not user.can("read")Request object: доступ к контексту теста
request.param — параметры из parametrize
@pytest.fixture
def resource(request):
param = request.param
return create_resource(param)request.node — информация о тесте
@pytest.fixture
def logger(request):
"""Логирует информацию о тесте"""
test_name = request.node.name
test_file = request.node.fspath
print(f"\n🧪 Running: {test_name} in {test_file}")
yield
print(f"✅ Finished: {test_name}")request.config — доступ к pytest config
@pytest.fixture
def base_url(request):
"""Получает base_url из pytest config"""
return request.config.getoption("--base-url", default="http://localhost")
def test_api(base_url):
response = requests.get(f"{base_url}/api/users")
assert response.status_code == 200Запуск:
pytest --base-url=https://api.example.comrequest.addfinalizer — альтернатива yield
@pytest.fixture
def resource(request):
"""Фикстура с finalizer"""
res = create_resource()
# Регистрируем cleanup
def cleanup():
res.close()
request.addfinalizer(cleanup)
return resПреимущество: Можно зарегистрировать несколько finalizers:
@pytest.fixture
def complex_resource(request):
resource = create_resource()
request.addfinalizer(lambda: resource.close())
request.addfinalizer(lambda: cleanup_files())
request.addfinalizer(lambda: send_metrics())
return resourceКомбинация: factory + indirect (bonus)
@pytest.fixture
def make_user():
"""Factory внутри фикстуры"""
users = []
def _make(role):
user = User(role=role)
users.append(user)
return user
yield _make
for user in users:
user.delete()
@pytest.fixture
def user(request, make_user):
"""Indirect фикстура использует factory"""
role = request.param
return make_user(role)
@pytest.mark.parametrize("user", ["admin", "moderator", "user"], indirect=True)
def test_permissions(user):
assert user.role in ["admin", "moderator", "user"]
# Все пользователи удалятся после теста благодаря factory!Что вы изучили
- Fixture factories — фикстуры возвращают функции для создания объектов
- indirect=True — параметризация фикстур
- request.param — доступ к параметрам из parametrize
- request object — node, config, addfinalizer
- Комбинация — factory + indirect для мощных паттернов
- Best practices — DRY для тестовых данных
Следующий урок
Отлично! Теперь вы умеете создавать гибкие фикстуры. Но как создать собственные маркеры для организации тестов?
Переходите к уроку 4: Custom markers и strict mode
В следующем уроке вы узнаете:
- Регистрация кастомных маркеров
--strict-markersдля защиты от опечаток- Маркеры с аргументами
- Best practices
Устранение неисправностей
Фикстура ожидает параметр из parametrize, но вы забыли indirect.
Как починить:
# ✅ ПРАВИЛЬНО
@pytest.mark.parametrize("user", ["admin"], indirect=True)
# ❌ НЕПРАВИЛЬНО
@pytest.mark.parametrize("user", ["admin"]) # Забыли indirect
Созданные сущности остаются в БД/файлах после теста.
Как починить:
@pytest.fixture
def make_user():
users = []
def _make(**kwargs):
user = User(**kwargs)
users.append(user)
return user
yield _make
for user in users:
user.delete()
По умолчанию indirect применится ко всем параметрам кортежа.
Как починить:
@pytest.mark.parametrize(
"user,data",
[("admin", {"key": "value"})],
indirect=["user"], # Только user indirect
)
def test(user, data):
...
Передавайте словари/объекты через pytest.param с id.
@pytest.mark.parametrize("user", [
pytest.param({"role": "admin", "age": 30}, id="admin-30"),
pytest.param({"role": "user", "age": 25}, id="user-25"),
], indirect=True)
def test(user):
...
@pytest.fixture
def user(request):
config = request.param # Это dict!
return User(**config)