conftest.py: переиспользование фикстур
У вас 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 автоматически:
- Находит
conftest.pyвtests/ - Загружает все фикстуры из него
- Делает их доступными для ВСЕХ тестов
Не нужно:
- ❌ Импортировать 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 == 1000autouse: автоматическое применение
Проблема: 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Как работает:
tests/conftest.py— фикстуры доступны ВЕЗДЕtests/api/conftest.py— дополнительные фикстуры только дляtests/api/*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 NonePytest автоматически находит и загружает все фикстуры из conftest.py. Импорт не нужен и даже вызовет ошибку!
Проверьте что:
- Файл называется ТОЧНО
conftest.py(неconfig.py) - Файл в директории
tests/или её поддиректориях - Фикстура определена с
@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()