Фикстуры 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
Одна фикстура на несколько тестов
Смысл фикстуры раскрывается, когда тестов становится больше: одна подготовка обслуживает весь файл. Добавим третий тест — и сравним оба подхода в таблице.
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)
А в один тест можно принять сразу несколько независимых фикстур — перечислить их аргументами через запятую. Отчёт по заказу, например, хочет и список заказов, и список пользователей:
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 - лежит в папке с тестами
import pytest
@pytest.fixture
def cart():
return {"хлеб": 2, "молоко": 1}
# тесты любого файла этой папки принимают cart
# аргументом - без импортов и копий
Что дальше
Теперь подготовка данных — не копипаста, а именованная функция: тест объявляет аргумент, 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))
1. Как тест получает данные из фикстуры orders?
2. Что делает @pytest.fixture?
3. Сколько раз вызовется фикстура для двух тестов, которые её принимают?
4. Тест объявил аргумент crt, а фикстура называется cart. Что будет?
5. Чем буква E в отчёте отличается от F?
6. Что может находиться внутри фикстуры?
Собери файл test_library.py с фикстурой books, возвращающей список ["Дюна", "Пикник"], и двумя тестами: test_first проверяет, что первый элемент — «Дюна», test_sorted — что sorted(books) даёт ["Дюна", "Пикник"]. Оба теста должны принимать фикстуру аргументом. Запусти и напечатай код выхода.
Что такое фикстура в 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-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
pytest · Урок 4
Несколько тестов в файле: независимость и порядок
Один файл — три теста — одно падение: упавший тест не отменяет соседей. Плюс порядок сверху вниз, помощники вместо копипасты и первый рефакторинг под защитой набора.
pytest · Урок 6
yield-фикстуры: уборка после теста (setup и teardown)
Тест создал файл — и кто-то должен убрать за ним. yield-фикстура держит подготовку и уборку в одной функции, а уборку выполняет всегда, даже когда тест упал.
pytest · Урок 7
Область видимости фикстур: scope и autouse
Сколько раз создаётся фикстура? По умолчанию — для каждого теста, но scope="module" экономит до одного раза на файл, а autouse применяет её вовсе без указания в аргументах.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Flask · Урок 16
Фикстуры для Flask-тестов: conftest, клиент и временная база
Шестнадцатый урок расширения Flask: каждый тест создаёт клиент сам — пора зафиксировать подготовку в фикстурах, вынести её в conftest.py и раздавать тестам временные базы через tmp_path.
pytest flask фикстуры conftestфикстура test_client flask
sqlite3 · Урок 8
UPDATE и DELETE в SQLite: меняем данные безопасно
Два самых ответственных оператора SQL: UPDATE и DELETE. Учимся менять и удалять строки так, чтобы не задеть чужие, — с rowcount как страховкой и мягким удалением.
sql изменить данные в таблицемягкое удаление в базе данных
pytest · Урок 15
conftest.py: общие фикстуры для всей папки
Фикстуры перестали быть домашними: conftest.py делает их общими для всей папки — без единого импорта, силами самого pytest.
pytest conftest.pyconftest.py общие фикстуры