Область видимости фикстур: scope и autouse
Сколько раз создаётся фикстура? По умолчанию — для каждого теста, но scope="module" экономит до одного раза на файл, а autouse применяет её вовсе без указания в аргументах.
Редакция Питоники
Из пятого урока мы вынесли правило: каждому тесту — свежий экземпляр фикстуры. Это безопасно, но не всегда дёшево: соединение с базой, запуск тестового сервера, чтение большого файла — каждое из них дорого, и создавать их по разу на тест расточительно. У pytest есть ручка и для этого: аргумент scope у декоратора фикстуры. Плюс вторая ручка — autouse — для фикстур, которые должны применяться ко всем тестам без просьб. Разберём обе с доказательствами, а не на словах.
По умолчанию: свежий экземпляр на каждый тест
Как доказать, что фикстура создаётся заново для каждого теста? Счётчиком. Положим в тестовый файл словарь-счётчик и будем увеличивать его в фикстуре — а тесты проверят его значение в момент выполнения.
from pathlib import Path
Path("test_scope.py").write_text("""import pytest
calls = {"cart": 0}
@pytest.fixture
def cart():
calls["cart"] += 1
return ["хлеб"]
def test_first(cart):
assert cart == ["хлеб"]
assert calls["cart"] == 1
def test_second(cart):
assert cart == ["хлеб"]
assert calls["cart"] == 2
""", encoding="utf-8")
import pytest
rc = pytest.main(["test_scope.py", "-q", "--no-header", "-p", "no:cacheprovider"])
print("exit:", int(rc))
.. [100%] 2 passed in 0.01s exit: 0
Оба теста зелёные, и по assert'ам видна механика: в первом тесте счётчик равен единице — фикстура отработала один раз. Во втором — двойке: pytest снова вызвал фикстуру, свежую корзину получил второй тест, а счётчик честно это задокументировал. Это и есть значение scope по умолчанию — "function": один вызов на одну тест-функцию.
scope="module": один вызов на файл
Теперь та же схема, но фикстуре задан scope="module" — «один экземпляр на модуль», то есть на весь тестовый файл. Счётчик снова в деле — ожидания у тестов другие:
from pathlib import Path
Path("test_scope.py").write_text("""import pytest
calls = {"orders": 0}
@pytest.fixture(scope="module")
def orders():
calls["orders"] += 1
return 3
def test_orders_count(orders):
assert orders == 3
assert calls["orders"] == 1
def test_orders_positive(orders):
assert orders > 0
assert calls["orders"] == 1
""", encoding="utf-8")
import pytest
rc = pytest.main(["test_scope.py", "-q", "--no-header", "-p", "no:cacheprovider"])
print("exit:", int(rc))
.. [100%] 2 passed in 0.01s exit: 0
Два теста прошли, и в обоих счётчик равен единице: фикстура выполнилась один раз на весь файл, а её результат раздаётся всем тестам модуля. Для дорогой подготовки это прямая экономия: одно соединение вместо тридцати, один запуск сервера вместо тридцати. Полный список значений scope небольшой:
| scope | Фикстура живёт | Типичное применение |
|---|---|---|
| "function" (по умолчанию) | один тест | данные, которые тест вправе менять |
| "module" | весь тестовый файл | дорогая подготовка: файл, конфиг, набор записей |
| "session" | весь прогон всех файлов | самое дорогое: соединение, тестовый сервер |
autouse: фикстура без аргумента
Обе фикстуры выше тесты принимали аргументом. А если подготовка нужна всем тестам файла, и перечислять её в каждом — шум? Флаг autouse=True включает фикстуру автоматически: тесты её не объявляют, а она всё равно выполняется перед каждым из них. Классика жанра — лог: записать, какой тест запустился.
from pathlib import Path
Path("test_log.py").write_text("""import pytest
LOG = []
@pytest.fixture(autouse=True)
def note(request):
LOG.append(request.node.name)
def test_sum():
assert sum([1, 2, 3]) == 6
assert LOG == ["test_sum"]
def test_len():
assert len("pytest") == 6
assert LOG[-1] == "test_len"
""", encoding="utf-8")
import pytest
rc = pytest.main(["test_log.py", "-q", "--no-header", "-p", "no:cacheprovider"])
print("exit:", int(rc))
.. [100%] 2 passed in 0.01s exit: 0
Две точки, 2 passed, и ни один тест не упомянул note в аргументах. Разбор трюка: autouse=True включает фикстуру в прогон каждого теста файла, а аргумент request — встроенный объект pytest, через который фикстура узнаёт контекст: request.node.name вернул имя запускаемого теста. Лог зафиксировал оба имени в порядке выполнения — снова сверху вниз, как в четвёртом уроке.
Когда общее состояние опасно
Теперь цена вопроса. Фикстура со scope="module" создаётся один раз — значит, все тесты файла получают один и тот же объект. Пока объект только читают, всё хорошо. Стоит его изменить — изменения доживут до следующих тестов. Проведём опыт: module-корзина, первый тест кладёт в неё товар, второй проверяет, что в ней оказалось.
from pathlib import Path
Path("test_leak.py").write_text("""import pytest
@pytest.fixture(scope="module")
def cart():
return ["хлеб"]
def test_add_item(cart):
cart.append("молоко")
assert cart == ["хлеб", "молоко"]
def test_sees_leftovers(cart):
assert cart == ["хлеб", "молоко"]
""", encoding="utf-8")
import pytest
rc = pytest.main(["test_leak.py", "-q", "--no-header", "-p", "no:cacheprovider"])
print("exit:", int(rc))
.. [100%] 2 passed in 0.01s exit: 0
Оба теста зелёные — и это плохая новость, замаскированная под хорошую. Второй тест увидел молоко, которое добавил первый: изменяемое состояние протекло между тестами. Набор стал зависимым от порядка: поменяй тесты местами — второй упадёт, потому что молока ещё нет. С фикстурой по умолчанию этот опыт невозможен: каждому тесту досталась бы своя корзина только с хлебом.
Сводим обе темы: scope и yield умеют работать в паре. Module-фикстура с уборкой выполняет код после yield один раз — после всех тестов файла. Это доказуемо тем же способом, каким мы проверяли уборку в шестом уроке: фикстура пишет файл на teardown, а скрипт проверяет его после прогона.
from pathlib import Path
Path("test_scope.py").write_text("""from pathlib import Path
import pytest
@pytest.fixture(scope="module")
def orders():
yield 3
Path("module_end.txt").write_text("фикстура закрылась после всех тестов файла", encoding="utf-8")
def test_orders_count(orders):
assert orders == 3
def test_orders_positive(orders):
assert orders > 0
""", encoding="utf-8")
import pytest
rc = pytest.main(["test_scope.py", "-q", "--no-header", "-p", "no:cacheprovider"])
print(Path("module_end.txt").read_text(encoding="utf-8"))
print("exit:", int(rc))
.. [100%] 2 passed in 0.01s фикстура закрылась после всех тестов файла exit: 0
Файл появился — значит, teardown module-фикстуры сработал, и сработал ровно один раз: после обоих тестов. Так строят долгоживущие ресурсы: соединение открывается на файл, а закрывается после последнего теста, даже если часть тестов упала — механизм уборки из шестого урока действует и на широком scope.
@pytest.fixture(scope="session")
def engine():
return "соединение с базой"
@pytest.fixture(scope="session")
def api():
return "запущенный тестовый сервер"
Широкий scope и autouse тоже сочетаются: @pytest.fixture(autouse=True, scope="module") — сквозная логика, выполняемая один раз на файл. Например, засечь время прогона файла целиком и записать в лог:
import time
import pytest
TIMES = []
@pytest.fixture(autouse=True, scope="module")
def stopwatch():
started = time.monotonic()
yield
TIMES.append(round(time.monotonic() - started, 2))
И последнее в сегодняшней программе: фикстуры не обязаны жить в самом тестовом файле. Вынеси их в conftest.py рядом с тестами — pytest подхватит файл сам, без импортов, а флаги scope и autouse работают там же. Это разыменовывает руки: общая подготовка папки описывается один раз, а не копируется по файлам.
# conftest.py - лежит в папке с тестами
import pytest
@pytest.fixture(scope="module")
def config():
return {"env": "test"}
@pytest.fixture(autouse=True)
def guard():
yield
# уборка после каждого теста папки
Что дальше
Ты умеешь управлять временем жизни фикстур: function — по умолчанию, module — на файл, session — на прогон; autouse — для сквозной логики без аргументов; и знаешь цену широкого scope — общее изменяемое состояние. Фикстуры освоены. Дальше параметризация: один тест и список случаев вместо десятка копий — второй мегаштабирующий приём pytest. А в одиннадцатом уроке встретимся со встроенными фикстурами capsys и tmp_path, которые pytest даёт бесплатно.
Scope — это обмен: платишь общим состоянием, получаешь экономию на подготовке. Покупай только то, что не мутирует.
Сначала предскажи ответ в голове — это главный навык программиста.
calls = 0
def fresh():
global calls
calls += 1
return calls
first = fresh()
second = fresh()
print(first, second)
cache = {}
def once(key, make):
if key not in cache:
cache[key] = make()
return cache[key]
first = once("db", lambda: "соединение")
second = once("db", lambda: "соединение")
print(first is second)
shared = ["хлеб"]
log = []
def use(item):
shared.append(item)
log.append(len(shared))
use("молоко")
use("сыр")
print(log)
1. Какой scope у фикстуры по умолчанию?
2. Сколько раз выполнится фикстура со scope="module" в файле из пяти тестов?
3. Что делает autouse=True у фикстуры?
4. Чем опасна изменяемая фикстура со scope="module"?
5. Что разумно выносить в scope="session"?
6. Что такое request в фикстуре note(request) из урока?
Собери файл test_pool.py с фикстурой pool со scope="module", которая возвращает список ["соединение"] и считает свои вызовы в словаре made. Напиши два теста: test_pool_alive проверяет содержимое и что made["pool"] == 1, test_pool_reused — первый элемент и снова единицу счётчика. Запусти и напечатай код выхода.
Сколько раз создаётся фикстура в pytest?
По умолчанию — один раз для каждого теста, который её принял: это scope="function". Аргументом scope декоратора время жизни расширяется: scope="module" — один экземпляр на весь тестовый файл, scope="session" — один на весь прогон. Доказать легко счётчиком: фикстура увеличивает счётчик, а тесты проверяют его значение — при module они увидят единицу.
Что такое autouse-фикстура и когда её применять?
Это фикстура с декоратором @pytest.fixture(autouse=True): pytest применяет её ко всем тестам файла автоматически, без объявления в аргументах. Применение — сквозная логика, нужная каждому тесту: журнал запусков, замер времени, сброс окружения. Через аргумент request фикстура узнаёт имя теста и другой контекст. Для данных конкретных тестов autouse избыточен — там уместны обычные фикстуры по имени.
Когда фикстуре со scope="module" или "session" становится опасно?
Когда её результат изменяем. Все тесты области получают один объект, и мутация из первого теста доживает до второго: набор молча становится зависимым от порядка выполнения — работает сейчас и падает после перестановки тестов. Правило: широким scope снабжают только дорогую неизменяемую подготовку — соединение, конфиг, прочитанный справочник; всё, что тесты меняют, живёт со scope по умолчанию.
Чем scope="module" отличается от scope="session"?
Границей времени жизни: module-фикстура создаётся один раз на тестовый файл и пересоздаётся в следующем файле, session-фикстура живёт весь прогон от первого до последнего теста всех файлов. На практике session достаётся самому дорогому — соединению с базой или запущенному тестовому серверу; в песочнице браузерных уроков почти всегда хватает function и module.
Понравился урок? Сошлитесь на него
«Scope — это обмен: платишь общим состоянием, получаешь экономию на подготовке. Покупай только то, что не мутирует.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
pytest · Урок 5
Фикстуры pytest: @pytest.fixture для подготовки данных
Одна и та же корзина собирается в каждом тесте заново — знакомая копипаста. Фикстура описывает подготовку один раз, а pytest сам передаёт результат в тест по имени.
pytest · Урок 6
yield-фикстуры: уборка после теста (setup и teardown)
Тест создал файл — и кто-то должен убрать за ним. yield-фикстура держит подготовку и уборку в одной функции, а уборку выполняет всегда, даже когда тест упал.
pytest · Урок 8
Параметризация: @pytest.mark.parametrize
Десять одинаковых тестов для десяти случаев — это десять копипаст. parametrize сжимает их в один тест и список кортежей, а pytest разворачивает обратно в десять отчётных строк.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Flask · Урок 16
Фикстуры для Flask-тестов: conftest, клиент и временная база
Шестнадцатый урок расширения Flask: каждый тест создаёт клиент сам — пора зафиксировать подготовку в фикстурах, вынести её в conftest.py и раздавать тестам временные базы через tmp_path.
pytest flask фикстуры conftestфикстура test_client flask
requests · Урок 8
Cookies и Session в requests: сессия, которая помнит
Разбираем, как сервер узнаёт клиента между запросами: Set-Cookie и Cookie руками на http.cookies, затем Session в requests — общие заголовки, куки-банка и keep-alive.
requests sessionrequests.Session
pytest · Урок 15
conftest.py: общие фикстуры для всей папки
Фикстуры перестали быть домашними: conftest.py делает их общими для всей папки — без единого импорта, силами самого pytest.
pytest conftest.pyconftest.py общие фикстуры