Session/module/function 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.
@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!
@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
# 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 | Когда использовать | Пример | Создаётся |
|---|---|---|---|
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 раз на файл!
# Хотим чтобы перед каждым тестом:
# - Очищался кеш
# - Сбрасывался счётчик
# - Логировалось имя теста@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
✅ Не нужно указывать фикстуру в параметрах!
# Перед каждым тестом
@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
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 → function
2. Зависимости: от родителя к ребёнку
@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 раз
@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)# ✅ ХОРОШО — переиспользуем БД
@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()@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]Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: Fixture factories и indirect parametrization