open atlas
↑ К треку
Python для JS/TS-разработчиков PY · 07 · 02

Фикстуры и parametrize: внедрение зависимостей, скоупы и порядок teardown

Фикстуры ищутся по имени и кэшируются на скоуп: session амортизирует дорогой setup, но делит изменяемое состояние — отказы от порядка тестов. yield раскручивает teardown в обратном порядке. parametrize складывается декартово; indirect=True ведёт параметры через фикстуры.

PY Senior ◷ 19 min
Уровень
ОсновыJuniorMiddleSenior

Биллинговый сьют месяцами был зелёным — до вторника, когда в CI упало 40 тестов, при том что каждый из них локально проходил. pytest tests/billing/test_invoices.py: 22 passed. Полный прогон: 40 failed, все — после test_upgrades.py в порядке сбора. Причина сидела в session-фикстуре tenant: один спровижененный словарь арендатора на весь прогон — 8 секунд подготовки, оплаченные один раз вместо трёхсот, осознанная и разумная оптимизация. Новый тест проверял апгрейд через tenant["plan"] = "enterprise" и ничего не возвращал назад. Каждый тест, разрешавший tenant после него, наследовал enterprise-арендатора: проверки квот промахивались на тариф, итоги инвойсов расходились — 40 красных тестов с трейсбеками, указывающими куда угодно, только не на причину. Два инженера потеряли день, бисектируя порядок тестов: под -k провалы исчезали, под -p no:randomly перетасовывались, а --lf перезапускал только жертв — и они проходили. Фикс занял три строки teardown. Урок — контракт скоупа: session-скоуп выдаёт каждому тесту один и тот же объект, и pytest не следит, что они с ним делают.

Фикстуры — внедрение зависимостей, разрешаемое по имени

Прежде чем перейти к механике: если ты когда-нибудь спрашивал «почему этот тест строит базу данных, которой никогда не пользуется», или обнаруживал teardown разбросанным по setUp, tearDown и пяти вспомогательным методам — фикстуры и есть ответ: они подключают ровно то, что тест запрашивает, и ничего сверх.

Тест объявляет свои потребности именами параметров; pytest разрешает каждое имя в фикстуру — сначала в модуле теста, затем вверх по цепочке conftest.py (ближайший побеждает), затем в плагинах, — вызывает её и кэширует результат на время её скоупа. Фикстуры запрашивают другие фикстуры тем же способом, так что исполняется граф зависимостей, построенный под тест, где каждый узел создаётся не более раза на единицу скоупа:

import pytest, sqlite3

@pytest.fixture
def db(tmp_path):                  # фикстуры компонуются: db зависит от tmp_path
    conn = sqlite3.connect(tmp_path / "t.db")
    yield conn                     # setup выше yield, teardown ниже
    conn.close()                   # выполнится, даже если тест упал

def test_insert(db):               # внедряется по ИМЕНИ — без импортов и реестров
    db.execute("create table t (x int)")

Поэтому фикстуры структурно сильнее методов setUp: тест называет ровно то, чем пользуется, неиспользуемая машинерия не строится, а одна фикстура обслуживает и один тест, и четыреста без гимнастики с наследованием. Фикстура request открывает контекст разрешения — запросивший тест, его маркеры, request.param — он понадобится параметризации ниже.

Скоупы: амортизация против изоляции

Скоуп — это ключ кэша. function (дефолт) пересобирает на каждый тест: полная изоляция, полная цена. module и session строят раз на модуль или на прогон; между ними есть class и package. Арифметика, толкающая команды к широким скоупам, реальна: Postgres-тестконтейнер со стартом ~8 с, 300 интеграционных тестов — function-скоуп тратит 40 минут на одну подготовку, session-скоуп — 8 секунд. Цена — инцидент из вступления: кэш возвращает один и тот же объект каждому потребителю, и одна мутация становится фоновым состоянием для всех последующих тестов. Это и есть класс отказов, зависящих от порядка: зелёный в одиночку, красный в сьюте, невоспроизводимый через --lf, потому что мутирующий тест не попадает в перезапуск. Стандартное решение слоистое: session-скоуп для движка или контейнера (дорогой, фактически неизменяемый) и function-скоуп для соединения, открывающего транзакцию и откатывающего её в teardown (дешёвый, идеально изолированный). Никогда не выдавайте тестам изменяемый session-объект напрямую; оборачивайте во function-скоупное представление, свежую копию или транзакцию.

Викторина

Session-фикстура возвращает словарь конфигурации арендатора. Один тест его мутирует. Каков наблюдаемый паттерн отказа?

yield-фикстуры: teardown, который выполняется всегда — и в обратном порядке

Всё выше yield — setup; всё ниже — teardown, и он выполняется, даже когда тест упал: финализацию фикстур ведёт стек финализаторов pytest, а не исход теста. Два точных края: если setup самой фикстуры поднял исключение, её teardown и не начинался, но фикстуры, успевшие завершить setup раньше, всё равно раскручиваются; и порядок teardown — обратный порядку разрешения: если тест разрешил db → schema → user, teardown идёт user → schema → db, как вложенные контекст-менеджеры. Зависимые освобождаются раньше зависимостей — единственный порядок, не закрывающий соединение, пока его кто-то держит. Широкие скоупы правило не ломают, а откладывают: session-скоупный db финализируется в конце сессии — всё равно после всех, кто от него зависел.

Фабричные фикстуры: вариативность на тест без игр со скоупами

Когда тестам нужны разные объекты, а не один общий, возвращайте строитель вместо значения — и держите очистку в фикстуре:

@pytest.fixture
def make_user(db):
    created = []
    def _make(plan="free", **kw):
        u = create_user(db, plan=plan, **kw)
        created.append(u)
        return u
    yield _make
    for u in reversed(created):     # LIFO, зеркало teardown фикстур
        delete_user(db, u)

def test_upgrade(make_user):
    u = make_user(plan="free")      # форму решает сам тест

Это чистый ответ на инцидент из вступления: тест апгрейда вызывает make_user(plan="free") и мутирует собственного приватного пользователя; общий арендатор не тронут. autouse=True — ход в обратную сторону: фикстура, внедряемая в каждый тест своего скоупа без упоминания по имени. У неё есть легитимные применения (заморозка времени, сидирование ГСЧ, изоляция окружения) и один системный изъян: невидимая связанность — читатель теста не видит, почему поведение отличается, а каждая будущая autouse-фикстура облагает налогом все тесты поддерева независимо от уместности. По умолчанию — явные имена. Слоение conftest.py следует тому же принципу локальности: ближайший conftest побеждает, так что tests/integration/conftest.py может переопределить settings только для своего поддерева.

parametrize: декартова машина

@pytest.mark.parametrize("pct", [0, 15, 100])
@pytest.mark.parametrize("cents", [1, 1999], ids=["min", "typical"])
def test_discount(cents, pct):       # декораторы перемножаются: 2 x 3 = 6 тестов
    ...

Стопка декораторов parametrize даёт декартово произведение — здесь 6 сгенерированных тестов, каждый — независимый элемент: падает поодиночке, виден в --lf и репортится как test_discount[typical-15] благодаря ids=. Без ids большие параметризации падают как test_discount[1999-15], и кто-то идёт грепать, что это был за случай. indirect=True проводит параметр через фикстуру: значение приходит в request.param внутри фикстуры, которая преобразует его до того, как тест его увидит, — идиома для «прогнать сьют против postgres и sqlite», где параметр — имя бэкенда, а фикстура строит настоящий движок. Тот же эффект с другой стороны: @pytest.fixture(params=["postgres", "sqlite"]) параметризует каждый зависящий тест без декоратора на месте вызова.

Ещё один внедряемый инструмент заслуживает места здесь: monkeypatch — function-скоупная фикстура с setattr, setitem, setenv, delenv, chdir, syspath_prepend — каждое изменение автоматически откатывается в teardown, ровно та гарантия, которой нет у ручных правок os.environ. За unittest.mock идите, когда нужны записи вызовов, side_effect или spec-проверенные дублёры: monkeypatch подменяет значения, mock наблюдает взаимодействия.

Викторина

Тест использует `user`, зависящий от `schema`, зависящей от `db` — три function-скоупные yield-фикстуры. Тест ПАДАЕТ. Какие teardown выполняются и в каком порядке?

Вспомните перед уходом
  1. 01
    Объясните механизм фикстур от и до: разрешение, кэширование — и почему session-фикстура дала провалы, видимые только в порядке полного прогона.
  2. 02
    Сформулируйте правила teardown для yield-фикстур, паттерн фабричной фикстуры и что делает indirect=True в parametrize.
Итог

Система фикстур — это внедрение зависимостей с кэшем, и каждое поведение в этом уроке выводится из этой фразы. Разрешение идёт по имени через модуль, цепочку conftest (ближайший побеждает) и плагины; ключ кэша — скоуп. Function-скоуп покупает изоляцию по полной цене; session-скоуп покупает восьмисекундный контейнер один раз вместо трёхсот и платит общим изменяемым состоянием — биллинговый инцидент, где одно tenant["plan"] = "enterprise" покрасило 40 нижестоящих тестов в порядке, который --lf даже не мог воспроизвести. Дисциплинированная форма кладёт неизменяемое широкоскоупное ядро под function-скоупные транзакционные представления — либо заменяет совместное использование фабричными фикстурами, которые строят объекты на тест и чистят их в обратном порядке после yield: то же правило LIFO, по которому живёт сам стек фикстур, зависимые раньше зависимостей, с провалом или без. autouse меняет видимость на удобство и должен быть редкостью; слоение conftest держит переопределения локальными. parametrize перемножает стопки декораторов в декартовы произведения с ids= для читаемых провалов, проводит значения через фикстуры с indirect=True или params=, а monkeypatch закрывает подмены окружения и атрибутов с гарантированным откатом, оставляя unittest.mock для случаев, когда нужно наблюдать вызовы, а не подменять значения. Теперь, когда встречаешь провалы, исчезающие под -k но стабильные в полном прогоне, первый вопрос звучит так: какая session-фикстура раздаёт тестам изменяемый объект без function-скоупной обёртки?

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.