Перейти к содержанию

172 уроков, 13 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 5 из 20 Средний 35 мин 130 XP

Фикстуры pytest: @pytest.fixture для подготовки данных

Одна и та же корзина собирается в каждом тесте заново — знакомая копипаста. Фикстура описывает подготовку один раз, а pytest сам передаёт результат в тест по имени.

Редакция Питоники

Тесты редко проверяют функцию в вакууме: им нужны данные — список заказов, корзина с товарами, словарь настроек. В четвёртом уроке мы выносили такую подготовку в помощников, и это уже неплохо. Но у pytest есть штатный механизм именно для подготовки: фикстуры. Они решают ту же задачу, что и помощники, но глубже интегрированы во фреймворк — и умеют то, чего у самодельных функций нет. Знакомимся.

Проблема: подготовка копируется в каждый тест

Начнём с боли. Два теста отдела заказов проверяют один и тот же список заказов — и каждый собирает его сам, честной копипастой:

копипаста подготовки в каждом тесте
from pathlib import Path

Path("test_orders.py").write_text("""def test_total():
    orders = [{"sum": 1200}, {"sum": 350}, {"sum": 90}]
    assert sum(o["sum"] for o in orders) == 1640

def test_big_orders():
    orders = [{"sum": 1200}, {"sum": 350}, {"sum": 90}]
    assert len([o for o in orders if o["sum"] > 500]) == 1
""", encoding="utf-8")

import pytest

rc = pytest.main(["test_orders.py", "-q", "--no-header", "-p", "no:cacheprovider"])
print("exit:", int(rc))
Вывод
..                                                                       [100%]
2 passed in 0.01s
exit: 0

Оба теста зелёные, но посмотри на строку с orders — она повторена дословно. Сегодня их две. Завтра тестов станет восемь, и строка повторится восемь раз. А послезавтра в заказы добавится поле id — и править придётся все восемь, с риском пропустить одно место. Пропустишь — тесты начнут проверять разные данные и врать в отчёте.

@pytest.fixture: подготовка в одном месте

Фикстура — обычная функция с декоратором @pytest.fixture. Она готовит данные и возвращает их. Магия — на стороне теста: он объявляет аргумент с именем фикстуры, и pytest сам вызывает подготовку, а результат кладёт в этот аргумент. Тот же набор, но без копипасты:

та же подготовка, но в фикстуре
from pathlib import Path

Path("test_orders.py").write_text("""import pytest

@pytest.fixture
def orders():
    return [{"sum": 1200}, {"sum": 350}, {"sum": 90}]

def test_total(orders):
    assert sum(o["sum"] for o in orders) == 1640

def test_big_orders(orders):
    assert len([o for o in orders if o["sum"] > 500]) == 1
""", encoding="utf-8")

import pytest

rc = pytest.main(["test_orders.py", "-q", "--no-header", "-p", "no:cacheprovider"])
print("exit:", int(rc))
Вывод
..                                                                       [100%]
2 passed in 0.02s
exit: 0

Разбираем анатомию. orders с декоратором @pytest.fixture — фикстура: она возвращает готовый список заказов. У test_total появился аргумент с именем orders — pytest ищет фикстуру с таким же именем, вызывает её и передаёт результат. Тест не вызывает фикстуру сам, не импортирует, не знает, как список устроен внутри: он просто объявляет, что ему нужен orders. Вывод тот же, что у копипаст-версии: две точки, 2 passed.

Здесь и спрятан главный приём урока: имя аргумента теста — это адрес фикстуры. Отсюда следствие, которое часто удивляет новичков: тесты передают не значения, а имена. Написал def test_x(anything) — pytest пойдёт искать фикстуру anything, а не подставит пустоту или ноль.

Внутри фикстуры разрешён любой код — она же обычная функция: собрать словарь из кортежей, посчитать поле total, прочитать файл. Подготовка может быть сколь угодно многоступенчатой — тестам всё равно, они получают готовый результат.

фикстура с подготовкой в несколько шагов
@pytest.fixture
def order():
    items = [("кофе", 350), ("сироп", 90)]
    total = sum(price for _, price in items)
    return {"items": items, "total": total}

def test_total(order):
    assert order["total"] == 440

def test_positions(order):
    assert len(order["items"]) == 2
Фрагмент без запуска: полный паттерн запуска через write_text и pytest.main — в блоках выше. В настоящем прогоне оба теста проходят: 2 passed.

Одна фикстура на несколько тестов

Смысл фикстуры раскрывается, когда тестов становится больше: одна подготовка обслуживает весь файл. Добавим третий тест — и сравним оба подхода в таблице.

фикстура корзины на три теста
from pathlib import Path

Path("test_cart.py").write_text("""import pytest

@pytest.fixture
def cart():
    return {"хлеб": 2, "молоко": 1}

def test_keys(cart):
    assert list(cart) == ["хлеб", "молоко"]

def test_counts(cart):
    assert cart["хлеб"] == 2

def test_positions(cart):
    assert len(cart) == 2
""", encoding="utf-8")

import pytest

rc = pytest.main(["test_cart.py", "-q", "--no-header", "-p", "no:cacheprovider"])
print("exit:", int(rc))
Вывод
...                                                                      [100%]
3 passed in 0.01s
exit: 0

Три точки, 3 passed: одна строка с составом корзины, три теста, каждому — собственный словарь. Поменяешь состав корзины — правка в одном месте, и все три теста пересобирают свои данные из новой версии. Именно за это фикстуры и любят: набор растёт, а описание данных — нет.

СценарийКопипаста в каждом тестеФикстура
Изменилась структура данныхправить в каждом тесте, риск пропуститьправка в одном месте
Новый тесткопировать подготовку зановообъявить аргумент по имени
Свежесть данныхкак повезёт с дисциплинойгарантирована pytest
Читаемость тестастены подготовки до проверкисразу видно, что проверяется

Ошибка имён: fixture not found

Механизм «имя аргумента = имя фикстуры» имеет ахиллесову пяту: опечатку. Напишешь в тесте crt вместо cart — pytest не найдёт фикстуру с таким именем и пометит тест ошибкой. Смотрим на настоящий отчёт такой ситуации:

опечатка в имени аргумента
from pathlib import Path

Path("test_cart.py").write_text("""import pytest

@pytest.fixture
def cart():
    return ["хлеб"]

def test_cart(crt):
    assert crt == ["хлеб"]
""", encoding="utf-8")

import pytest

rc = pytest.main(["test_cart.py", "-q", "--no-header", "--tb=no", "-p", "no:cacheprovider"])
print("exit:", int(rc))
Вывод
E                                                                        [100%]
=========================== short test summary info ===========================
ERROR test_cart.py::test_cart
1 error in 0.01s
exit: 1

На месте точки — буква E (error): тест не упал на assert, он вообще не смог запуститься — подготовка сорвалась. В сводке строка ERROR test_cart.py::test_cart и итог 1 error, код выхода 1. Разница важная: F — проверка не прошла, E — тест даже не добрался до проверки. Полный отчёт без --tb=no назвал бы причину прямо: фикстура crt не найдена.

Фикстуры умеют принимать другие фикстуры — так собираются цепочки подготовки, где одна на другом строится:

цепочка: фикстура принимает фикстуру
@pytest.fixture
def prices():
    return [100, 250, 40]

@pytest.fixture
def total(prices):        # аргумент - тоже имя фикстуры
    return sum(prices)
Фрагмент без запуска: pytest сам разрулит порядок — сначала подготовит prices, потом передаст его в total. Полный прогон такой цепочки показан в шестом уроке.

А в один тест можно принять сразу несколько независимых фикстур — перечислить их аргументами через запятую. Отчёт по заказу, например, хочет и список заказов, и список пользователей:

две фикстуры в одном тесте
from pathlib import Path

Path("test_report.py").write_text("""import pytest

@pytest.fixture
def orders():
    return [{"sum": 1200}, {"sum": 350}]

@pytest.fixture
def users():
    return ["anna"]

def test_report(orders, users):
    assert len(users) == 1
    assert sum(o["sum"] for o in orders) == 1550
""", encoding="utf-8")

import pytest

rc = pytest.main(["test_report.py", "-q", "--no-header", "-p", "no:cacheprovider"])
print("exit:", int(rc))
Вывод
.                                                                        [100%]
1 passed in 0.01s
exit: 0

Оба аргумента пришли по именам, каждую фикстуру pytest подготовил отдельно — и в шестом уроке ты увидишь, что уборка у таких фикстур выполняется в обратном порядке. Осталось последнее ограничение сегодняшнего урока: фикстуры живут внутри своего тестового файла. Для общих фикстур целой папки у pytest есть отдельный файл — conftest.py; зафиксируем знакомство, а подробно — в пятнадцатом уроке.

conftest.py — общие фикстуры папки
# conftest.py - лежит в папке с тестами
import pytest

@pytest.fixture
def cart():
    return {"хлеб": 2, "молоко": 1}

# тесты любого файла этой папки принимают cart
# аргументом - без импортов и копий
Фрагмент без запуска: pytest находит conftest.py сам и раздаёт его фикстуры всем тестам папки. Полный разбор — в уроке 15.

Что дальше

Теперь подготовка данных — не копипаста, а именованная функция: тест объявляет аргумент, pytest подставляет свежий результат, одна фикстура обслуживает весь файл. Не хватает одной вещи: кто убирает за тестом, если фикстура создала файл? Ответ — yield-фикстуры, тема шестого урока: код до yield готовит, код после — убирает, даже если тест упал. А в седьмом разберём, как одной фикстурой обслужить весь файл разом — scope и autouse.

Фикстура — это помощник, которого pytest взял в штат: тест называет имя, фреймворк приносит свежие данные.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

def fixture_data():
    return ["хлеб"]

data_a = fixture_data()
data_a.append("молоко")
data_b = fixture_data()
print(data_b == ["хлеб"])
fixtures = {"cart": "корзина", "orders": "заказы"}

def resolve(arg):
    if arg in fixtures:
        return fixtures[arg]
    raise ValueError("not found: " + arg)

print(resolve("orders"))
def make_cart():
    return {"хлеб": 2}

a = make_cart()
b = make_cart()
a["молоко"] = 1
print(len(b))
Проверь себя
0 / 6

1. Как тест получает данные из фикстуры orders?

2. Что делает @pytest.fixture?

3. Сколько раз вызовется фикстура для двух тестов, которые её принимают?

4. Тест объявил аргумент crt, а фикстура называется cart. Что будет?

5. Чем буква E в отчёте отличается от F?

6. Что может находиться внутри фикстуры?

Карточки терминов
Запомнено: 0 / 6
Практика

Собери файл test_library.py с фикстурой books, возвращающей список ["Дюна", "Пикник"], и двумя тестами: test_first проверяет, что первый элемент — «Дюна», test_sorted — что sorted(books) даёт ["Дюна", "Пикник"]. Оба теста должны принимать фикстуру аргументом. Запусти и напечатай код выхода.

practice.py
Вопросы и ответы по уроку

Что такое фикстура в pytest простыми словами?

Это функция с декоратором @pytest.fixture, которая готовит данные для тестов: список заказов, словарь настроек, содержимое файла. Тест объявляет аргумент с именем фикстуры — и pytest сам вызывает подготовку, передавая результат в тест. Каждый тест получает свежий экземпляр данных, а сама подготовка описана в коде ровно один раз.

Чем фикстура отличается от обычной функции-помощника?

Помощника тест вызывает сам и отвечает за него; фикстуру pytest вызывает по имени аргумента и гарантирует свежий экземпляр для каждого теста. Фикстуры интегрированы во фреймворк: их можно переопределять для части тестов, выносить в общий conftest.py, цеплять к набору автоматически через autouse и добавлять уборку после теста через yield. Для сложной и разделяемой подготовки фикстуры удобнее во всём.

Почему pytest пишет fixture 'cart' not found и как это исправить?

Так фреймворк сообщает, что тест объявил аргумент, для которого нет фикстуры с точным таким именем: чаще всего это опечатка (crt вместо cart) или фикстура, объявленная в другом файле без общего conftest.py. Исправление — привести имя аргумента к имени фикстуры. В кратком отчёте ошибка выглядит как буква E и строка ERROR в секции сводки.

Могут ли несколько тестов использовать одну фикстуру?

Да, это основной сценарий: одна фикстура обслуживает весь файл, каждый тест объявляет её по имени и получает собственный свежий экземпляр данных. Правка структуры данных делается в одном месте, и все тесты автоматически пересобирают данные из новой версии. Один экземпляр сразу на несколько тестов настраивается отдельным механизмом — scope="module", о нём в седьмом уроке.

Понравился урок? Сошлитесь на него

«Фикстура — это помощник, которого pytest взял в штат: тест называет имя, фреймворк приносит свежие данные.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.