Skip to main content
Back to course
Pytest для джунов: Моки и интеграция
7 / 888%

conftest.py: переиспользование фикстур

20 минут

У вас 5 файлов тестов. В каждом дублируется фикстура client для HTTP-клиента. Как вынести её в одно место?

Цель: Научиться использовать conftest.py для переиспользования фикстур.

Вы точно готовы?

Убедитесь, что умеете:

# Создавать фикстуры
@pytest.fixture
def database():
    db = connect_to_db()
    yield db
    db.close()
 
# Использовать фикстуры
def test_user(database):
    user = database.get_user(1)
    assert user.name == "Alice"

Если фикстуры непонятны — вернитесь к pytest-basics урок 6.

Проблема: дублирование фикстур

Без conftest.py

# tests/test_api.py
 
@pytest.fixture
def client():
    return APIClient(base_url="https://api.example.com")
 
def test_get_users(client):
    users = client.get("/users")
    assert len(users) > 0
 
# tests/test_auth.py
 
@pytest.fixture  # ❌ Дублирование!
def client():
    return APIClient(base_url="https://api.example.com")
 
def test_login(client):
    result = client.post("/login", {"user": "alice"})
    assert result["token"] is not None
 
# tests/test_products.py
 
@pytest.fixture  # ❌ Ещё раз дублирование!
def client():
    return APIClient(base_url="https://api.example.com")
 
def test_get_products(client):
    products = client.get("/products")
    assert len(products) > 0

Проблемы:

  • ❌ Фикстура client скопирована в 3 файлах
  • ❌ Если изменится base_url — нужно менять в 3 местах
  • ❌ Нарушение DRY (Don't Repeat Yourself)

Решение: conftest.py

Создание conftest.py

# tests/conftest.py
 
import pytest
from src.api_client import APIClient
 
@pytest.fixture
def client():
    """HTTP-клиент для API"""
 
    return APIClient(base_url="https://api.example.com")
 
@pytest.fixture
def database():
    """Подключение к БД"""
 
    db = connect_to_db()
    yield db
    db.close()

Теперь все тесты могут использовать эти фикстуры:

# tests/test_api.py
 
def test_get_users(client):  # ✅ Фикстура из conftest.py
    users = client.get("/users")
    assert len(users) > 0
 
# tests/test_auth.py
 
def test_login(client):  # ✅ Та же фикстура
    result = client.post("/login", {"user": "alice"})
    assert result["token"] is not None

✅ Фикстура определена ОДИН раз, используется ВЕЗДЕ!

Как работает conftest.py

Pytest автоматически:

  1. Находит conftest.py в tests/
  2. Загружает все фикстуры из него
  3. Делает их доступными для ВСЕХ тестов

Не нужно:

  • ❌ Импортировать conftest.py
  • ❌ Указывать путь к нему

✅ Pytest находит его автоматически!

Множественные фикстуры

# tests/conftest.py
 
import pytest
from unittest.mock import Mock
 
@pytest.fixture
def client():
    """API-клиент"""
 
    return APIClient(base_url="https://api.example.com")
 
@pytest.fixture
def mock_requests():
    """Мок для requests"""
 
    import requests_mock
    with requests_mock.Mocker() as m:
        yield m
 
@pytest.fixture
def test_user():
    """Тестовый пользователь"""
 
    return {
        "id": 1,
        "name": "Alice",
        "email": "alice@example.com"
    }
 
@pytest.fixture
def test_product():
    """Тестовый продукт"""
 
    return {
        "id": 101,
        "name": "Laptop",
        "price": 1000
    }

Использование:

# tests/test_orders.py
 
def test_create_order(test_user, test_product):
    """Используем обе фикстуры"""
 
    order = create_order(user=test_user, product=test_product)
    assert order.total == 1000

autouse: автоматическое применение

Проблема: setup нужен в КАЖДОМ тесте

# Хотим чтобы перед каждым тестом:
# 1. Очищалась БД
# 2. Применялись migrations
# 3. Создавались тестовые данные

Решение: autouse=True

# tests/conftest.py
 
@pytest.fixture(autouse=True)
def setup_database():
    """Выполняется ПЕРЕД каждым тестом автоматически"""
 
    # Setup
    db = connect_to_db()
    db.drop_all_tables()
    db.create_tables()
    db.apply_migrations()
 
    yield
 
    # Teardown
    db.drop_all_tables()
    db.close()

Теперь:

# tests/test_users.py
 
def test_create_user():
    # setup_database выполнился АВТОМАТИЧЕСКИ!
    user = User.create(name="Alice")
    assert user.id is not None

✅ Не нужно указывать setup_database в параметрах!

Когда использовать autouse

✅ Хорошие случаи:

  • Очистка БД перед каждым тестом
  • Настройка логирования
  • Установка переменных окружения
  • Инициализация кешей

❌ Плохие случаи:

  • Медленные операции (БД для ВСЕХ тестов)
  • Фикстуры нужные только некоторым тестам

Пример: logging setup

@pytest.fixture(autouse=True, scope="session")
def setup_logging():
    """Настройка логирования один раз для всей сессии"""
 
    import logging
    logging.basicConfig(level=logging.INFO)

Иерархия conftest.py

Множественные conftest.py

tests/
├── conftest.py          # Глобальные фикстуры (для ВСЕХ тестов)
├── test_root.py
├── api/
│   ├── conftest.py      # Фикстуры для API-тестов
│   ├── test_users.py
│   └── test_products.py
└── unit/
    ├── conftest.py      # Фикстуры для unit-тестов
    ├── test_calculator.py
    └── test_utils.py

Как работает:

  1. tests/conftest.py — фикстуры доступны ВЕЗДЕ
  2. tests/api/conftest.py — дополнительные фикстуры только для tests/api/*
  3. tests/unit/conftest.py — дополнительные фикстуры только для tests/unit/*

Практический пример

# tests/conftest.py (глобальные)
 
@pytest.fixture
def database():
    """БД для всех тестов"""
 
    return connect_to_db()
 
# tests/api/conftest.py (только для API-тестов)
 
@pytest.fixture
def api_client(database):
    """API-клиент с доступом к БД"""
 
    return APIClient(db=database)
 
@pytest.fixture
def auth_headers():
    """Заголовки аутентификации"""
 
    return {"Authorization": "Bearer fake-token"}
 
# tests/unit/conftest.py (только для unit-тестов)
 
@pytest.fixture
def mock_database():
    """Мок БД для unit-тестов"""
 
    from unittest.mock import Mock
    return Mock()

Использование:

# tests/api/test_users.py
 
def test_get_users(api_client, auth_headers):
    # ✅ api_client из tests/api/conftest.py
    # ✅ Может использовать database из tests/conftest.py
 
    response = api_client.get("/users", headers=auth_headers)
    assert response.status_code == 200
 
# tests/unit/test_calculator.py
 
def test_add():
    # ✅ Не видит api_client (он только для tests/api/)
    # ✅ Может использовать mock_database из tests/unit/conftest.py
 
    assert add(2, 3) == 5

Порядок загрузки

Pytest загружает conftest.py от корня к листьям:

1. tests/conftest.py
2. tests/api/conftest.py
3. Тест: tests/api/test_users.py

✅ Фикстуры ближе к тесту могут переопределить глобальные!

# tests/conftest.py
 
@pytest.fixture
def base_url():
    return "https://api.example.com"
 
# tests/api/conftest.py
 
@pytest.fixture
def base_url():
    return "https://api.test.com"  # Переопределяет глобальную!
 
# tests/api/test_users.py
 
def test_users(base_url):
    print(base_url)  # https://api.test.com ✅

Scope и conftest.py

Scope для производительности

# tests/conftest.py
 
@pytest.fixture(scope="session")
def database():
    """БД создаётся ОДИН раз для всей сессии"""
 
    print("🔌 Connecting to database...")
    db = connect_to_db()
    yield db
    print("🧹 Closing database...")
    db.close()
 
@pytest.fixture(scope="module")
def api_client(database):
    """API-клиент создаётся один раз для каждого модуля"""
 
    print("🚀 Creating API client...")
    return APIClient(db=database)
 
@pytest.fixture(scope="function")
def test_user():
    """Новый пользователь для КАЖДОГО теста"""
 
    print("👤 Creating test user...")
    return User(name="TestUser")

Запускаем 3 теста в одном файле:

🔌 Connecting to database...     # session (один раз)
🚀 Creating API client...        # module (один раз на файл)
👤 Creating test user...         # function (каждый тест)
test 1 PASSED
👤 Creating test user...
test 2 PASSED
👤 Creating test user...
test 3 PASSED
🧹 Closing database...           # session teardown

✅ База создаётся ОДИН раз, пользователь — для каждого теста!

Рекомендации по scope

ScopeКогда использоватьПример
functionЧистое состояние для каждого теста (default)test_user, test_product
classТесты в одном классеДля Test классов
moduleМедленный setup для одного файлаAPI client, mock server
sessionОчень медленный setup (один раз)Database, Docker container

Практический пример: реальный проект

# tests/conftest.py (глобальный)
 
import pytest
from src.database import Database
from src.cache import RedisCache
 
@pytest.fixture(scope="session")
def database():
    """PostgreSQL БД (один раз для всех тестов)"""
 
    db = Database("postgresql://localhost/test_db")
    db.create_tables()
    yield db
    db.drop_tables()
    db.close()
 
@pytest.fixture(scope="session")
def redis():
    """Redis кеш (один раз для всех тестов)"""
 
    cache = RedisCache(host="localhost", port=6379, db=1)
    yield cache
    cache.flushdb()
    cache.close()
 
@pytest.fixture(autouse=True)
def clean_database(database):
    """Очищаем БД перед каждым тестом"""
 
    database.truncate_all_tables()
 
# tests/api/conftest.py
 
@pytest.fixture
def api_client(database, redis):
    """API-клиент с БД и кешем"""
 
    from src.api import APIClient
    return APIClient(db=database, cache=redis)
 
@pytest.fixture
def auth_token():
    """JWT токен для аутентификации"""
 
    from src.auth import create_token
    return create_token(user_id=1, role="admin")
 
# tests/unit/conftest.py
 
@pytest.fixture
def mock_database():
    """Мок БД для unit-тестов"""
 
    from unittest.mock import Mock
    mock_db = Mock()
    mock_db.get_user.return_value = {"id": 1, "name": "Alice"}
    return mock_db

Использование:

# tests/api/test_users.py
 
def test_create_user(api_client, auth_token):
    """Integration-тест с реальной БД"""
 
    response = api_client.post(
        "/users",
        json={"name": "Bob"},
        headers={"Authorization": f"Bearer {auth_token}"}
    )
    assert response.status_code == 201
 
# tests/unit/test_user_service.py
 
def test_get_user(mock_database):
    """Unit-тест с моком"""
 
    from src.services import UserService
    service = UserService(db=mock_database)
 
    user = service.get_user(1)
 
    assert user["name"] == "Alice"
    mock_database.get_user.assert_called_once_with(1)

Что вы изучили

  • conftest.py — общие фикстуры для всех тестов
  • autouse=True — автоматическое применение фикстур
  • Иерархия — множественные conftest.py в поддиректориях
  • Scope — session/module/function для производительности
  • Переопределение — локальные фикстуры заменяют глобальные
  • Организация — разделение по типам тестов (api/unit)

Следующий урок

Поздравляю! Вы изучили 7 из 8 уроков pytest-junior!

Теперь вы умеете мокировать (HTTP, время, файлы), измерять coverage, настраивать pytest.ini и организовывать фикстуры. Пора применить всё вместе!

Переходите к уроку 7: Мини-проект: Todo с кешем и API

В следующем уроке вы:

  • Создадите реальное приложение Todo
  • Напишите тесты с моками, fixtures, coverage
  • Используете всё что изучили в курсе
  • Получите готовый шаблон для своих проектов

Устранение неисправностей

НЕТ! Фикстуры из conftest.py доступны автоматически.

# tests/conftest.py
@pytest.fixture
def client():
  return APIClient()

# tests/test_api.py

# ❌ НЕ НУЖНО импортировать!

# from conftest import client

# ✅ Просто используйте как параметр

def test_users(client):
assert client.get("/users") is not None

Pytest автоматически находит и загружает все фикстуры из conftest.py. Импорт не нужен и даже вызовет ошибку!

Проверьте что:

  1. Файл называется ТОЧНО conftest.py (не config.py)
  2. Файл в директории tests/ или её поддиректориях
  3. Фикстура определена с @pytest.fixture
# ✅ ПРАВИЛЬНО
@pytest.fixture(autouse=True)
def setup():
pass

# ❌ НЕПРАВИЛЬНО

@pytest.fixture
def setup(autouse=True): # autouse не аргумент функции!
pass

Это нормально! Фикстуры из tests/api/conftest.py доступны ТОЛЬКО в tests/api/*. Переместите общие фикстуры в tests/conftest.py.

Можно! Фикстуры из родительского conftest.py доступны в дочернем:

# tests/conftest.py
@pytest.fixture
def database():
return connect_to_db()

# tests/api/conftest.py

@pytest.fixture
def api_client(database): # ✅ Использует database из родителя
return APIClient(db=database)

Убедитесь что фикстура не зависит от фикстуры с меньшим scope:

# ❌ НЕПРАВИЛЬНО
@pytest.fixture(scope="session")
def api_client(test_user): # test_user имеет scope="function"!
return APIClient(user=test_user)

# ✅ ПРАВИЛЬНО

@pytest.fixture(scope="session")
def api_client():
return APIClient()
conftest.py: переиспользование фикстур — Pytest для джунов: Моки и интеграция — Potapov.me