
Область действия fixture в pytest: как найти зависимость тестов друг от друга


Если тест по отдельности проходит, а в полном наборе падает, проблема часто не в самом assert, а в состоянии, которое осталось после соседнего теста. В pytest это обычно связано не с магией запуска, а с тем, что два теста получили один и тот же изменяемый объект: список, словарь, экземпляр класса или клиент с внутренним кэшем.
Разобраться помогает не перебор случайных правок, а короткая диагностика. Сначала нужно понять, какой объект переживает один тест и доживает до другого. Затем — проверить, не расширили ли вы область действия fixture шире, чем нужно. И только после этого решать, что менять: scope, подготовку данных или схему очистки.
Как распознать, что тесты зависят друг от друга
У pytest фикстуры передаются в тесты по имени аргумента. По умолчанию fixture имеет область действия function, то есть создается для одного теста и уничтожается после него. Поэтому сам по себе факт использования fixture еще не означает утечку состояния. Наоборот, стандартное поведение как раз помогает держать тесты изолированными.
Подозрение на зависимость появляется, когда картина выглядит так: тест в одиночном запуске зеленый, а после другого теста — красный; падение исчезает, если удалить соседний тест; в сообщении об ошибке видно неожиданное значение, которое могло накопиться ранее. Это типичный след общего изменяемого объекта, а не отдельной логической ошибки в одном сценарии.
Еще один признак — тесты начинают вести себя по-разному после замены scope с function на module, class или session. Более широкий scope полезен для дорогих по созданию ресурсов, но он же делает возможным повторное использование одного и того же экземпляра между несколькими тестами. Если этот экземпляр кто-то меняет, соседний тест уже стартует не с нуля.
Где искать общее изменяемое состояние
В первую очередь смотрите на fixture, которые возвращают не просто данные, а объект с внутренним состоянием. Риск выше у списков, словарей, множеств, экземпляров классов с полями и у любых оберток, где есть методы вроде append, update, increment, login, set_token. Если такой объект живет дольше одного теста, его легко испортить незаметно.
Второе место поиска — autouse-fixture. Они срабатывают автоматически, поэтому зависимость не видна в сигнатуре теста. Тест может выглядеть «чистым», но на деле до него уже отработала fixture, которая что-то добавила в общую коллекцию, включила флаг или изменила состояние клиента. Для диагностики это неприятно: источник проблемы скрыт, хотя механизм тот же самый.
Полезно не перепутать две разные ситуации. В документации pytest отдельно сказано, что внутри одного теста значение fixture кэшируется: если эту fixture запрашивают и сам тест, и другая fixture, они увидят один и тот же объект. Это нормально и не считается утечкой между тестами. Настоящая проблема начинается тогда, когда один и тот же объект переживает границу между двумя тестовыми функциями.
- Проверьте fixture с
scope="module",class,packageиsession. - Ищите изменяющие операции внутри тестов:
append, присваивание полям, изменение счетчиков, очистку и повторное заполнение. - Просмотрите
autouse-fixture и зависимые fixture, которые могут менять общий объект до начала assert.
Что именно делает scope fixture
Scope отвечает не за порядок тестов, а за срок жизни объекта. При function каждый тест получает свежий экземпляр. При module один экземпляр может быть переиспользован всеми тестами в файле. При session — вообще всеми тестами тестовой сессии, которые запросят эту fixture. Для быстрого сетевого клиента или тяжелого подключения это может экономить время, но для изменяемых тестовых данных создает лишний риск.
Из-за этого диагностика получается довольно прямой. Если вы временно сужаете область действия подозрительной fixture до function, а нестабильное падение исчезает, вы нашли сильную подсказку: тесты делили один и тот же объект. Это еще не окончательное исправление, потому что нужно понять, где именно объект меняют, но направление уже ясно.
Если широкий scope действительно нужен, обычно разделяют ресурс и данные. Например, можно держать модульный клиент или подключение, но создавать отдельную сущность данных на каждый тест через function-scoped fixture и удалять ее после теста. Для очистки удобно использовать yield-fixture: код до yield готовит данные, код после — убирает их. Важно именно то, что состояние данных должно жить не дольше одного теста, даже если общий клиент живет дольше.
Учебный пример: как один модульный объект ломает соседний тест
Учебный пример. Среда: Python 3.11 и pytest 8.x. Исходные данные условные: есть объект-счетчик, у которого начальное значение 0, а метод increment() увеличивает его на 1. Один тест проверяет увеличение счетчика, второй ожидает, что новый счетчик начинается с нуля.
# app.py
class Counter:
def __init__(self):
self.total = 0
def increment(self):
self.total += 1
# test_counter.py
import pytest
from app import Counter
@pytest.fixture(scope="module")
def counter():
return Counter()
def test_increment(counter):
counter.increment()
assert counter.total == 1
def test_starts_from_zero(counter):
assert counter.total == 0
Здесь ошибка не в арифметике. Ошибка в том, что fixture counter модульная, а значит оба теста могут получить один и тот же экземпляр Counter. Если первый тест успел вызвать increment(), второй увидит уже total == 1 и упадет. При этом одиночный запуск test_starts_from_zero пройдет, потому что для него будет создан новый счетчик с нулем.
| Режим проверки | Что получает test_starts_from_zero | Результат |
|---|---|---|
| Одиночный запуск второго теста | Новый Counter с total = 0 | Проходит |
| Запуск всего файла при модульной fixture | Тот же Counter после increment() | Падает |
| Запуск всего файла после возврата к function scope | Отдельный Counter для каждого теста | Оба проходят |
Проверяемый результат диагностики простой: замените fixture на функцию без явного scope, то есть вернитесь к значению по умолчанию.
@pytest.fixture
def counter():
return Counter()
После этого каждый тест снова начнет с total == 0. Если падение исчезло, значит вы подтвердили гипотезу о разделяемом состоянии, а не случайно «починили» тест изменением порядка.
Как перепроверить гипотезу запуском в изоляции и другим порядком
Самая полезная последовательность такая: сначала запустите подозрительный тест отдельно, потом вместе с тем тестом, который мог изменить состояние, затем временно сузьте scope. Если поведение меняется именно на этих шагах, у вас не абстрактная «нестабильность», а конкретная зависимость от соседа.
Дальше можно проверить чувствительность к порядку. Если проект использует стандартную коллекцию pytest без дополнительных плагинов, в учебном файле достаточно временно переставить две тестовые функции местами. Если после перестановки падает уже другой сценарий или падение исчезает, это не повод оставить новый порядок как есть. Это лишь подтверждение, что тесты делят состояние и что один из них стартует не с чистых данных.
Исправлять такую ситуацию лучше не косметически, а по причине. Обычно помогает один из трех вариантов: вернуть fixture к function-scope, перестать менять объект прямо в тесте, либо отделить дорогой общий ресурс от короткоживущих тестовых данных. Простое правило звучит так: общий объект может быть широкого scope, только если тесты не оставляют в нем следов, влияющих на соседей.
- Проверьте, проходит ли тест в одиночном запуске.
- Найдите fixture с широким scope или скрытым
autouse-эффектом. - Временно замените scope на
function. - Если проблема исчезла, отделите общий ресурс от данных, которые должны быть свежими на каждый тест.
- Подтвердите вывод повторным запуском пары тестов и, при необходимости, временной перестановкой функций в учебном файле.
Если хотите отдельно потренировать работу с fixture, на внутренней карточке курса «Автотестировщик на Python» по данным официальной страницы школы есть проект: написать 3 теста с помощью PyTest для сайта gismeteo.ru и вынести действия из тестового метода в фикстуры. Для темы с утечкой состояния это полезное продолжение: сначала понять, почему общий объект ломает изоляцию, потом выносить подготовку данных так, чтобы тесты не зависели друг от друга.




















