Перейти к содержимому
К программе курса
Pytest с нуля: Первые тесты за 2.5 часа
7 / 888%

Fixtures: создаём данные один раз

25 минут

Вы уже умеете группировать тесты маркерами и запускать с разными данными. Но что если один и тот же 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:

  1. Видит параметр account в тесте
  2. Находит фикстуру с именем account
  3. Вызывает фикстуру ПЕРЕД тестом
  4. Передаёт результат в тест как аргумент

✅ Теперь 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 == 1500

Scope фикстур: когда создавать объекты

По умолчанию: 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) ✅ Default
  • module — медленные объекты (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 для stacktrace
  • pytest --pdb для отладки
  • Как читать сообщения об ошибках

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

Фикстура должна быть объявлена и доступна тесту

Проверьте:

  • Есть декоратор @pytest.fixture над определением
  • Имя аргумента в тесте совпадает с названием фикстуры
  • Фикстура находится в том же файле или в conftest.py

Используйте подходящий scope для дорогих ресурсов

Проверьте:

  • Для кэша на файл задайте scope="module"
  • Для всего запуска — scope="session"

Вынесите создание ресурса в фикстуру и верните его через yield, а после yield выполните уборку

Проверьте:

  • Создайте ресурс перед yield
  • После yield удалите или закройте ресурс

Либо параметризуйте фикстуру, либо заведите несколько разных фикстур

Проверьте:

  • Используйте @pytest.fixture(params=[...]) и request.param
  • Создайте отдельные фикстуры, например small_account/large_account
Fixtures: создаём данные один раз — Pytest с нуля: Первые тесты за 2.5 часа — Potapov.me