Fixtures: создаём данные один раз
Вы уже умеете группировать тесты маркерами и запускать с разными данными. Но что если один и тот же setup повторяется в 10 тестах?
Цель: Научиться использовать фикстуры для переиспользования setup-кода и избавиться от дублирования.
Вы точно готовы?
Убедитесь, что умеете:
# Параметризация
@pytest.mark.parametrize("value", [1, 2, 3])
def test_with_params(value):
assert value > 0
# Маркеры
@pytest.mark.slow
def test_slow_operation():
passЕсли parametrize или markers непонятны — вернитесь к урокам 4-5.
Проблема: дублирование setup-кода
Плохой код: копипаста в каждом тесте
# ❌ ПЛОХО — повторяем создание account в каждом тесте
def test_deposit():
account = BankAccount(100) # Дублирование
account.deposit(50)
assert account.balance == 150
def test_withdraw():
account = BankAccount(100) # Дублирование
account.withdraw(30)
assert account.balance == 70
def test_multiple_deposits():
account = BankAccount(100) # Дублирование
account.deposit(50)
account.deposit(20)
assert account.balance == 170Проблемы:
- ❌ Копипаста:
BankAccount(100)повторяется 3 раза - ❌ Если нужно изменить initial_balance — менять во всех тестах
- ❌ Сложно поддерживать при росте тестов
Решение: @pytest.fixture
Базовая фикстура
import pytest
from src.bank_account import BankAccount
@pytest.fixture
def account():
"""Создаёт BankAccount с балансом 100"""
return BankAccount(100)
# ✅ ХОРОШО — используем фикстуру
def test_deposit(account):
"""Тест пополнения"""
account.deposit(50)
assert account.balance == 150
def test_withdraw(account):
"""Тест снятия"""
account.withdraw(30)
assert account.balance == 70
def test_multiple_deposits(account):
"""Тест множественных пополнений"""
account.deposit(50)
account.deposit(20)
assert account.balance == 170Что делает pytest:
- Видит параметр
accountв тесте - Находит фикстуру с именем
account - Вызывает фикстуру ПЕРЕД тестом
- Передаёт результат в тест как аргумент
✅ Теперь setup в одном месте!
Как это работает
@pytest.fixture
def account():
print("🔧 Setup: создаём account")
return BankAccount(100)
def test_deposit(account):
print("🧪 Тест запущен")
account.deposit(50)
assert account.balance == 150Вывод:
🔧 Setup: создаём account
🧪 Тест запущен
PASSEDФикстура вызывается ПЕРЕД каждым тестом!
Множественные фикстуры
Несколько фикстур в одном тесте
@pytest.fixture
def empty_account():
"""Пустой счёт"""
return BankAccount(0)
@pytest.fixture
def rich_account():
"""Счёт с большим балансом"""
return BankAccount(10000)
def test_transfer(empty_account, rich_account):
"""Перевод денег между счетами"""
# Arrange
amount = 500
# Act
rich_account.withdraw(amount)
empty_account.deposit(amount)
# Assert
assert rich_account.balance == 9500
assert empty_account.balance == 500Фикстура использует другую фикстуру
@pytest.fixture
def user():
"""Создаёт пользователя"""
return User("Alice", 25)
@pytest.fixture
def user_with_account(user):
"""Создаёт пользователя СО счётом"""
account = BankAccount(1000)
user.account = account
return user
def test_user_can_deposit(user_with_account):
"""Пользователь может пополнить счёт"""
user_with_account.account.deposit(500)
assert user_with_account.account.balance == 1500Scope фикстур: когда создавать объекты
По умолчанию: scope="function"
@pytest.fixture # scope="function" по умолчанию
def account():
print("🔧 Создаём account")
return BankAccount(100)
def test_deposit(account):
print("🧪 test_deposit")
pass
def test_withdraw(account):
print("🧪 test_withdraw")
passРезультат:
🔧 Создаём account
🧪 test_deposit
PASSED
🔧 Создаём account
🧪 test_withdraw
PASSED✅ Каждый тест получает НОВЫЙ объект!
scope="module" — один объект для всех тестов
@pytest.fixture(scope="module")
def database_connection():
"""Подключение к БД (медленно!)"""
print("🔌 Подключаемся к БД...")
connection = connect_to_db()
return connection
def test_insert_user(database_connection):
print("🧪 test_insert_user")
pass
def test_select_user(database_connection):
print("🧪 test_select_user")
passРезультат:
🔌 Подключаемся к БД...
🧪 test_insert_user
PASSED
🧪 test_select_user
PASSED✅ Подключение создано ОДИН раз для всего модуля!
Scope опции
@pytest.fixture(scope="function") # Каждый тест (default)
@pytest.fixture(scope="class") # Один раз для класса
@pytest.fixture(scope="module") # Один раз для файла
@pytest.fixture(scope="session") # Один раз для всей сессииКогда использовать:
function— быстрые объекты (User, BankAccount) ✅ Defaultmodule— медленные объекты (DB connection, API client)session— очень медленные (Docker container, test database)
Fixtures с cleanup: yield (bonus)
Проблема: нужно закрыть ресурсы
@pytest.fixture
def temp_file():
"""Создаёт временный файл"""
file = open("test.txt", "w")
file.write("test data")
file.close()
return "test.txt"
# ❌ Файл останется после теста!Решение: yield для cleanup
@pytest.fixture
def temp_file():
"""Создаёт временный файл и удаляет после теста"""
# Setup
filename = "test.txt"
with open(filename, "w") as f:
f.write("test data")
yield filename # Возвращаем в тест
# Teardown (выполнится ПОСЛЕ теста)
import os
os.remove(filename)
print("🧹 Cleanup: файл удалён")
def test_read_file(temp_file):
"""Тест чтения файла"""
with open(temp_file, "r") as f:
content = f.read()
assert content == "test data"
# После этого теста файл удалится!Порядок выполнения:
1. Setup: создаём файл
2. yield filename → передаём в тест
3. Тест выполняется
4. Teardown: удаляем файлПрактический пример: полный набор фикстур
# tests/conftest.py (фикстуры доступны во ВСЕХ тестах)
import pytest
from src.bank_account import BankAccount
from src.user import User
@pytest.fixture
def empty_account():
"""Пустой банковский счёт"""
return BankAccount(0)
@pytest.fixture
def account():
"""Счёт с начальным балансом"""
return BankAccount(100)
@pytest.fixture
def rich_account():
"""Счёт с большим балансом"""
return BankAccount(10000)
@pytest.fixture
def user():
"""Пользователь"""
return User("Alice", 25)
@pytest.fixture
def user_with_account(user, account):
"""Пользователь со счётом"""
user.account = account
return user
@pytest.fixture(scope="module")
def database():
"""Подключение к БД (медленно)"""
print("🔌 Connecting to database...")
db = connect_to_test_db()
yield db
print("🧹 Closing database connection")
db.close()Использование:
# tests/test_bank_account.py
def test_deposit_to_empty(empty_account):
"""Пополнение пустого счёта"""
empty_account.deposit(100)
assert empty_account.balance == 100
def test_withdraw_from_account(account):
"""Снятие со счёта"""
account.withdraw(50)
assert account.balance == 50
def test_user_account_deposit(user_with_account):
"""Пользователь пополняет счёт"""
user_with_account.account.deposit(500)
assert user_with_account.account.balance == 600
def test_save_to_database(account, database):
"""Сохранение счёта в БД"""
database.save(account)
loaded = database.load(account.id)
assert loaded.balance == 100Где хранить фикстуры
Вариант 1: В том же файле с тестами
# tests/test_bank_account.py
@pytest.fixture
def account():
return BankAccount(100)
def test_deposit(account):
passКогда: Фикстура используется ТОЛЬКО в этом файле.
Вариант 2: В conftest.py (глобально)
# tests/conftest.py
@pytest.fixture
def account():
return BankAccount(100)Когда: Фикстура используется в НЕСКОЛЬКИХ файлах.
✅ Pytest автоматически находит conftest.py и загружает фикстуры!
Что вы изучили
- @pytest.fixture — переиспользование setup-кода
- Scope фикстур — function, module, session
- yield — cleanup после тестов
- conftest.py — глобальные фикстуры
- Композиция фикстур — фикстура использует другую фикстуру
- Избавились от дублирования — DRY в тестах
Следующий урок
Поздравляю! Вы изучили 7 из 8 основных уроков pytest-basics!
Теперь вы умеете писать тесты, параметризовать их, группировать маркерами и переиспользовать setup через фикстуры. Но что делать когда тест падает? Как быстро найти причину?
Переходите к уроку 7: Отладка: понимаем ошибки
В следующем уроке вы узнаете:
pytest -vи-vvдля детального вывода--tb=shortи--tb=longдля stacktracepytest --pdbдля отладки- Как читать сообщения об ошибках
Устранение неисправностей
Фикстура должна быть объявлена и доступна тесту
Проверьте:
- Есть декоратор @pytest.fixture над определением
- Имя аргумента в тесте совпадает с названием фикстуры
- Фикстура находится в том же файле или в conftest.py
Используйте подходящий scope для дорогих ресурсов
Проверьте:
- Для кэша на файл задайте scope="module"
- Для всего запуска — scope="session"
Вынесите создание ресурса в фикстуру и верните его через yield, а после yield выполните уборку
Проверьте:
- Создайте ресурс перед yield
- После yield удалите или закройте ресурс
Либо параметризуйте фикстуру, либо заведите несколько разных фикстур
Проверьте:
- Используйте @pytest.fixture(params=[...]) и request.param
- Создайте отдельные фикстуры, например small_account/large_account