Fixture factories для переиспользования. Indirect parametrization. Scope strategies: session/module/function для разных ресурсов. Request object паттерны. pytest.ini: кастомные маркеры и coverage
В этом уроке углубимся в продвинутые возможности pytest fixtures. Изучим паттерны, которые используются в production-проектах.
Цель урока: Писать гибкие, переиспользуемые fixtures.
Проблема: Иногда нужно создать несколько похожих объектов с разными параметрами.
# Плохой подход: дублирование
@pytest.fixture
def user1(db_connection):
return create_user(db_connection, "user1@test.com")
@pytest.fixture
def user2(db_connection):
return create_user(db_connection, "user2@test.com")Решение: Factory fixture возвращает функцию!
# conftest.py
@pytest.fixture
def user_factory(db_connection):
"""Фабрика для создания users"""
created_users = []
def _create_user(email, name="Test User"):
cur = db_connection.cursor()
cur.execute(
"INSERT INTO users (email, name) VALUES (%s, %s) RETURNING id",
(email, name)
)
user_id = cur.fetchone()[0]
cur.close()
created_users.append(user_id)
return user_id
yield _create_user
# Cleanup: можно удалить всех созданных users
# (но rollback в db_connection это сделает)
# test_with_factory.py
def test_multiple_users(user_factory):
"""Создаём несколько users одной фикстурой"""
alice = user_factory("alice@test.com", "Alice")
bob = user_factory("bob@test.com", "Bob")
charlie = user_factory("charlie@test.com", "Charlie")
assert alice != bob != charlieИдея: Запустить один тест с разными fixtures.
# conftest.py
@pytest.fixture(params=["postgresql", "mysql", "sqlite"])
def database_type(request):
"""Тестируем с разными БД"""
return request.param
def test_with_different_databases(database_type):
"""Тест запустится 3 раза: для postgresql, mysql, sqlite"""
print(f"Testing with {database_type}")
assert database_type in ["postgresql", "mysql", "sqlite"]Запуск:
pytest test.py -v
# test_with_different_databases[postgresql] PASSED
# test_with_different_databases[mysql] PASSED
# test_with_different_databases[sqlite] PASSEDПрактический пример:
@pytest.fixture(params=[True, False])
def completed_status(request):
return request.param
def test_task_with_different_status(client, completed_status):
"""Тестируем создание задачи с completed=True и False"""
response = client.post(
"/tasks",
json={"title": "Test", "completed": completed_status}
)
assert response.status_code == 201
assert response.json["completed"] == completed_statusrequest — специальный объект pytest с метаданными о тесте.
@pytest.fixture
def smart_fixture(request):
"""Фикстура знает кто её вызывает"""
test_name = request.node.name
print(f"Called by test: {test_name}")
# Можно передать параметры через indirect
if hasattr(request, "param"):
print(f"Parameter: {request.param}")
yield test_name
def test_example(smart_fixture):
assert "test_example" in smart_fixtureIndirect parametrization:
@pytest.fixture
def user_with_tasks(request, db_connection):
"""Создаёт user с N задачами"""
task_count = request.param # Получаем параметр
user_id = create_user(db_connection, f"user{task_count}@test.com")
for i in range(task_count):
create_task(db_connection, user_id, f"Task {i}")
return user_id
@pytest.mark.parametrize("user_with_tasks", [1, 5, 10], indirect=True)
def test_user_task_count(user_with_tasks, db_connection):
"""Тест запустится 3 раза: с 1, 5, 10 задачами"""
cur = db_connection.cursor()
cur.execute("SELECT COUNT(*) FROM tasks WHERE user_id=%s", (user_with_tasks,))
count = cur.fetchone()[0]
# count будет 1, потом 5, потом 10
assert count in [1, 5, 10]
cur.close()Scopes:
function (default) — новая фикстура для каждого тестаclass — одна фикстура для всех тестов в классеmodule — одна фикстура для всего файлаsession — одна фикстура для всего запуска@pytest.fixture(scope="session")
def db_schema():
"""Создаём схему БД один раз для всей сессии"""
conn = psycopg2.connect(...)
cur = conn.cursor()
cur.execute("CREATE TABLE IF NOT EXISTS users (...)")
conn.commit()
cur.close()
conn.close()
yield
# Cleanup после всех тестов
@pytest.fixture(scope="module")
def test_data(db_connection):
"""Создаём данные один раз для модуля"""
user_id = create_user(db_connection, "module_user@test.com")
return user_id
@pytest.fixture(scope="function")
def isolated_user(db_connection):
"""Новый user для каждого теста"""
return create_user(db_connection, "test@example.com")Когда использовать:
session — дорогие операции (запуск Docker, создание схемы)module — данные которые не меняются между тестамиfunction — когда нужна изоляцияautouse=True — фикстура выполняется автоматически для всех тестов.
@pytest.fixture(scope="function", autouse=True)
def reset_db(db_connection):
"""Очищаем БД перед каждым тестом"""
cur = db_connection.cursor()
cur.execute("TRUNCATE tasks, users RESTART IDENTITY CASCADE")
db_connection.commit()
cur.close()
yield
def test_something():
"""БД уже очищена, не нужно указывать reset_db"""
passОсторожно: autouse может замедлить тесты. Используйте только когда нужно.
Fixtures могут зависеть друг от друга:
@pytest.fixture
def db_connection():
conn = psycopg2.connect(...)
yield conn
conn.rollback()
conn.close()
@pytest.fixture
def user(db_connection):
"""Зависит от db_connection"""
return create_user(db_connection, "user@test.com")
@pytest.fixture
def task(db_connection, user):
"""Зависит от db_connection И user"""
return create_task(db_connection, user, "Test task")
def test_task_exists(task, db_connection):
"""Автоматически получаем: db_connection → user → task"""
cur = db_connection.cursor()
cur.execute("SELECT title FROM tasks WHERE id=%s", (task,))
assert cur.fetchone()[0] == "Test task"
cur.close()@pytest.fixture
def postgres_only(db_connection):
"""Фикстура только для PostgreSQL"""
cur = db_connection.cursor()
cur.execute("SELECT version()")
version = cur.fetchone()[0]
cur.close()
if "PostgreSQL" not in version:
pytest.skip("This test requires PostgreSQL")
yield
def test_postgres_specific_feature(postgres_only, db_connection):
"""Тест пропустится если не PostgreSQL"""
cur = db_connection.cursor()
cur.execute("SELECT version()")
# ... postgres-specific test
cur.close()# ❌ Плохо
@pytest.fixture
def f1():
pass
# ✅ Хорошо
@pytest.fixture
def authenticated_user():
passtests/
conftest.py # Общие fixtures для всех тестов
test_api.py
unit/
conftest.py # Fixtures только для unit тестов
test_models.py
integration/
conftest.py # Fixtures только для integration
test_api_db.py
# ❌ Плохо: autouse для редко используемой фикстуры
@pytest.fixture(autouse=True)
def expensive_setup():
time.sleep(5) # Замедлит ВСЕ тесты
# ✅ Хорошо: явно указываем где нужно
@pytest.fixture
def expensive_setup():
time.sleep(5)
def test_that_needs_it(expensive_setup):
pass@pytest.fixture
def blog_with_data(db_connection):
"""
Создаёт блог с предзаполненными данными:
- 3 users
- 2 posts на каждого user
- 1 comment на каждый post
Returns:
dict: {"users": [id1, id2, id3], "posts": [id1, id2, ...]}
"""
# ...✅ Factory fixtures — создание множества объектов ✅ Fixture parametrization — запуск тестов с разными fixtures ✅ Request object — метаданные о тесте ✅ Scope strategies — function, module, session ✅ autouse — автоматическое выполнение fixtures ✅ Зависимости — fixtures могут зависеть друг от друга ✅ Best practices — именование, структура, документация
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: Production CI/CD и quality gates