Skip to main content
Back to course
Pytest: Профессиональные инструменты
3 / 838%

Продвинутые фикстуры: scope и autouse

25 минут

У вас 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, Order100 раз для 100 тестов
classТесты в одном классеShared state для Test* класса1 раз на класс
moduleМедленный setup для одного файлаDatabase, Cache1 раз на .py файл
sessionОчень медленный setupDocker, Selenium browser1 раз для всех тестов

Практический пример: многоуровневые фикстуры

# 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 → 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

Порядок:

  1. database (session) — создаётся первым
  2. 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
Продвинутые фикстуры: scope и autouse — Pytest: Профессиональные инструменты — Potapov.me