Динамическое создание фикстур, indirect для параметризации фикстур, request object паттерны
Нужно протестировать функцию с 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=[])Проблемы:
@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")✅ Одна фикстура, любые вариации!
@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 пользователя удалятся автоматически!@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"# Хочу запустить тест с разными пользователями
@pytest.mark.parametrize("user", ["admin", "moderator", "user"])
def test_access(user):
# user — это строка "admin", не объект User!
assert user.can("read") # ❌ Не работает@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)"admin" в фикстуру user через request.paramUser(role="admin")User✅ Тест запустится 3 раза с разными пользователями!
@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
@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")@pytest.fixture
def resource(request):
param = request.param
return create_resource(param)@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}")@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.com@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@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!Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: Custom markers и strict mode