Продвинутые фикстуры: scope и autouse
У вас 100 тестов. Каждый создаёт БД соединение. Setup занимает 2 секунды. Итого 200 секунд только на setup! Как оптимизировать?
Цель: Освоить продвинутые техники работы с фикстурами для максимальной производительности.
Вы точно готовы?
Убедитесь, что умеете:
# Базовые фикстуры
@pytest.fixture
def database():
db = connect_to_db()
yield db
db.close()
def test_query(database):
result = database.query("SELECT * FROM users")
assert len(result) > 0Если базовые фикстуры непонятны — вернитесь к pytest-basics урок 6.
Проблема: медленные фикстуры
Код с дорогим setup
@pytest.fixture
def database():
"""Подключение к БД — медленно!"""
print("🔌 Connecting to database... (2 seconds)")
db = connect_to_db() # 2 секунды
yield db
print("🧹 Closing connection...")
db.close()
def test_user_1(database):
assert database.query("SELECT * FROM users") is not None
def test_user_2(database):
assert database.query("SELECT count(*) FROM users") > 0
# ... ещё 98 тестовЗапуск:
pytest tests/test_database.py -vРезультат:
🔌 Connecting... (2s)
test_user_1 PASSED
🧹 Closing...
🔌 Connecting... (2s)
test_user_2 PASSED
🧹 Closing...
# ... повторяется 100 раз❌ 200 секунд только на setup!
Решение: scope оптимизация
scope="session" — один раз для всех тестов
@pytest.fixture(scope="session")
def database():
"""Создаётся ОДИН раз для всей сессии"""
print("🔌 Connecting to database...")
db = connect_to_db()
yield db
print("🧹 Closing connection...")
db.close()Запуск 100 тестов:
🔌 Connecting... (1 раз!)
test_user_1 PASSED
test_user_2 PASSED
...
test_user_100 PASSED
🧹 Closing... (1 раз!)
==================== 100 passed in 50s ====================✅ 50 секунд вместо 200! Ускорение 4x
Scope уровни
# 1. function (default) — каждый тест
@pytest.fixture(scope="function")
def temp_user():
user = create_user()
yield user
delete_user(user)
# 2. class — один раз для класса
@pytest.fixture(scope="class")
def api_client():
client = APIClient()
yield client
client.close()
# 3. module — один раз для файла
@pytest.fixture(scope="module")
def database():
db = connect_to_db()
yield db
db.close()
# 4. session — один раз для всей сессии
@pytest.fixture(scope="session")
def docker_container():
container = start_docker()
yield container
stop_docker(container)Когда использовать какой scope
| Scope | Когда использовать | Пример | Создаётся |
|---|---|---|---|
function | Чистое состояние для каждого теста (default) | User, Order | 100 раз для 100 тестов |
class | Тесты в одном классе | Shared state для Test* класса | 1 раз на класс |
module | Медленный setup для одного файла | Database, Cache | 1 раз на .py файл |
session | Очень медленный setup | Docker, Selenium browser | 1 раз для всех тестов |
Практический пример: многоуровневые фикстуры
# conftest.py
@pytest.fixture(scope="session")
def docker_postgres():
"""Docker контейнер PostgreSQL — очень медленно"""
print("🐳 Starting PostgreSQL container... (10s)")
container = start_postgres_docker()
yield container
print("🧹 Stopping container...")
stop_docker(container)
@pytest.fixture(scope="module")
def database(docker_postgres):
"""БД подключение — медленно"""
print(f"🔌 Connecting to {docker_postgres.host}... (2s)")
db = connect_to_db(docker_postgres.connection_string)
yield db
print("🧹 Closing connection...")
db.close()
@pytest.fixture(scope="function")
def clean_database(database):
"""Очищаем БД перед каждым тестом"""
print("🧹 Truncating tables...")
database.truncate_all()
yield databaseИспользование:
# tests/test_users.py (10 тестов)
def test_create_user(clean_database):
user = create_user(clean_database, name="Alice")
assert user.id is not None
# ... ещё 9 тестовПорядок выполнения:
🐳 Starting PostgreSQL container... (1 раз для всех!)
🔌 Connecting... (1 раз для test_users.py)
🧹 Truncating tables... (10 раз — для каждого теста)
test_create_user PASSED
🧹 Truncating tables...
test_get_user PASSED
...
🧹 Closing connection... (1 раз после всех тестов модуля)
🧹 Stopping container... (1 раз в конце сессии)✅ Docker запускается 1 раз, БД создаётся 1 раз на файл!
autouse: автоматические фикстуры
Проблема: setup нужен ВСЕГДА
# Хотим чтобы перед каждым тестом:
# - Очищался кеш
# - Сбрасывался счётчик
# - Логировалось имя тестаРешение: autouse=True
@pytest.fixture(autouse=True)
def clear_cache():
"""Выполняется ПЕРЕД каждым тестом автоматически"""
print("🧹 Clearing cache...")
cache.clear()
def test_user_caching():
# clear_cache уже выполнился!
user = get_user(1)
assert user is not None
def test_product_caching():
# clear_cache снова выполнился!
product = get_product(1)
assert product is not NoneЗапуск:
🧹 Clearing cache...
test_user_caching PASSED
🧹 Clearing cache...
test_product_caching PASSED✅ Не нужно указывать фикстуру в параметрах!
autouse с разными scope
# Перед каждым тестом
@pytest.fixture(autouse=True, scope="function")
def setup_test():
print("▶️ Test starting...")
yield
print("✅ Test finished")
# Один раз для модуля
@pytest.fixture(autouse=True, scope="module")
def setup_module():
print("📦 Module setup...")
yield
print("📦 Module teardown")
# Один раз для сессии
@pytest.fixture(autouse=True, scope="session")
def setup_session():
print("🚀 Session starting...")
yield
print("🏁 Session finished")Запуск:
🚀 Session starting...
📦 Module setup...
▶️ Test starting...
test_1 PASSED
✅ Test finished
▶️ Test starting...
test_2 PASSED
✅ Test finished
📦 Module teardown
🏁 Session finishedПрактические примеры autouse
1. Логирование тестов
@pytest.fixture(autouse=True)
def log_test(request):
"""Логирует каждый тест"""
test_name = request.node.name
print(f"\n▶️ Starting: {test_name}")
yield
print(f"✅ Finished: {test_name}")2. Установка переменных окружения
@pytest.fixture(autouse=True, scope="session")
def setup_env():
"""Устанавливает ENV для всех тестов"""
os.environ["ENV"] = "test"
os.environ["DEBUG"] = "1"
yield
del os.environ["ENV"]
del os.environ["DEBUG"]3. Очистка состояния
@pytest.fixture(autouse=True)
def reset_state():
"""Сбрасывает глобальное состояние"""
yield
# После теста
global_cache.clear()
global_counter.reset()Порядок выполнения фикстур
Правила порядка
1. Scope: от широкого к узкому
session → module → class → function2. Зависимости: от родителя к ребёнку
@pytest.fixture(scope="session")
def database():
return connect_to_db()
@pytest.fixture(scope="function")
def clean_db(database): # Зависит от database
database.truncate()
return databaseПорядок:
database(session) — создаётся первымclean_db(function) — использует database
Визуализация порядка
@pytest.fixture(scope="session", autouse=True)
def a():
print("1. Session setup")
yield
print("8. Session teardown")
@pytest.fixture(scope="module", autouse=True)
def b():
print("2. Module setup")
yield
print("7. Module teardown")
@pytest.fixture(autouse=True)
def c():
print("3. Function setup")
yield
print("6. Function teardown")
def test_example():
print("4. Test body")
assert True
print("5. Test finished")Запуск:
1. Session setup
2. Module setup
3. Function setup
4. Test body
5. Test finished
6. Function teardown
7. Module teardown
8. Session teardown✅ LIFO (Last In, First Out) для teardown!
Стратегии оптимизации
Стратегия 1: Медленные ресурсы — session scope
# ✅ ХОРОШО — создаётся 1 раз
@pytest.fixture(scope="session")
def docker_container():
container = start_docker() # 30 секунд
yield container
stop_docker(container)
# ❌ ПЛОХО — создаётся 100 раз
@pytest.fixture # scope="function" по умолчанию
def docker_container():
container = start_docker() # 30 * 100 = 3000 секунд!
yield container
stop_docker(container)Стратегия 2: Изоляция через cleanup, не recreation
# ✅ ХОРОШО — переиспользуем БД
@pytest.fixture(scope="module")
def database():
db = connect_to_db() # 1 раз
yield db
db.close()
@pytest.fixture(autouse=True)
def clean_database(database):
# Очищаем, не пересоздаём!
database.truncate_all()
# ❌ ПЛОХО — пересоздаём БД каждый раз
@pytest.fixture
def database():
db = connect_to_db() # 100 раз
yield db
db.close()Стратегия 3: Ленивая загрузка данных
@pytest.fixture(scope="session")
def database():
"""Подключение — 1 раз"""
return connect_to_db()
@pytest.fixture(scope="module")
def test_data(database):
"""Тестовые данные — 1 раз на модуль"""
return load_test_data(database)
@pytest.fixture
def user(test_data):
"""Конкретный user — на каждый тест"""
return test_data["users"][0]Что вы изучили
- Scope уровни — session, module, class, function
- Оптимизация — правильный scope экономит время
- autouse=True — автоматические фикстуры
- Порядок выполнения — от широкого scope к узкому
- Стратегии — переиспользование, изоляция через cleanup
- Best practices — медленные ресурсы в session scope
Следующий урок
Отлично! Теперь фикстуры работают максимально эффективно. Но что если нужно динамически создавать фикстуры с разными параметрами?
Переходите к уроку 3: Fixture factories и indirect parametrization
В следующем уроке вы узнаете:
- Fixture factories для динамического создания
indirect=Trueдля параметризации фикстурrequest.paramдля доступа к параметрам- Продвинутые паттерны
Устранение неисправностей
Так и задумано: session scope запускается один раз на весь ран.
Как починить:
- Добавьте autouse фикстуру с function scope для очистки
@pytest.fixture(scope="session")
def database():
return connect_to_db()
@pytest.fixture(autouse=True)
def clean_db(database):
database.truncate_all()
Фикстура с широким scope не может тянуть зависимость с меньшим.
Как починить:
- Повысить scope зависимой фикстуры до module/session
- Или понизить scope верхней фикстуры до function
# ❌ ОШИБКА
@pytest.fixture(scope="session")
def app(temp_user): # temp_user = function scope
return App(user=temp_user)
# ✅ ПРАВИЛЬНО
@pytest.fixture(scope="session")
def app():
return App()
Автоматический setup цепляется ко всем тестам.
Как починить:
- Перенесите autouse в conftest.py нужной поддиректории
- Сделайте фикстуру явной и вызывайте только в нужных тестах
tests/
├── conftest.py # Глобальные autouse
└── api/
├── conftest.py # autouse только для API тестов
└── test_users.py
Pytest делает teardown в обратном порядке (LIFO) — так и должно быть.
Как настроить порядок:
- Добавьте finalizer вручную, если нужен специфичный порядок
@pytest.fixture
def resource(request):
res = create_resource()
request.addfinalizer(lambda: cleanup_resource(res))
return res