Общие фикстуры в conftest.py, scope, автоиспользование. DRY для тестов
У вас 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.
# 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 местах# 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✅ Фикстура определена ОДИН раз, используется ВЕЗДЕ!
Pytest автоматически:
conftest.py в tests/Не нужно:
✅ 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# Хотим чтобы перед каждым тестом:
# 1. Очищалась БД
# 2. Применялись migrations
# 3. Создавались тестовые данные# 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 в параметрах!
✅ Хорошие случаи:
❌ Плохие случаи:
Пример: logging setup
@pytest.fixture(autouse=True, scope="session")
def setup_logging():
"""Настройка логирования один раз для всей сессии"""
import logging
logging.basicConfig(level=logging.INFO)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) == 5Pytest загружает 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 ✅# 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 | Когда использовать | Пример |
|---|---|---|
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)Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: Мини-проект: Todo с кешем и API