Зависимость от порядка тестов.
Тест проходит или падает в зависимости от того, какие другие тесты выполнялись до него, потому что тесты «протекают» и полагаются на общее изменяемое состояние вместо того, чтобы каждый сам создавал и убирал свою фикстуру.
##Signs and Symptoms
Результат теста зависит от порядка, в котором выполняется набор, а не только от тестируемого кода. Предполагается, что каждый тест самодостаточен, но здесь один тест незаметно полагается на побочные эффекты, оставленные другим.
Характерные признаки:
- Тест проходит в полном наборе, но падает при запуске в одиночку (через
.only, фильтр по имени или запуск только этого файла). Месарош называет это одиноким тестом (Lonely Test). - Тесты ломаются, когда вы перемешиваете, шардируете или распараллеливаете запуск, или после обновления раннера, меняющего порядок по умолчанию.
- Удаление, пропуск или перестановка одного теста приводит к падению, казалось бы, не связанного с ним теста.
- Одно падение запускает каскад последующих падений (взаимодействующие тесты (Interacting Tests) у Месароша; война тестовых прогонов (Test Run War) у ван Дёрсена, когда параллельные раннеры сталкиваются из-за общей фикстуры).
- Тесты периодически нестабильны (flaky) без изменения кода — зависимость от порядка одна из самых частых первопричин нестабильных тестов.
Структурный признак — общее изменяемое состояние, которое читается между тестами: переменные уровня модуля/static/глобальные, beforeAll, который однократно заполняет состояние, или несброшенный внешний ресурс (строки БД, файлы, кэши, переменные окружения, поддельные таймеры, моки).
let users = []; // общее состояние уровня модуля
test('creates a user', () => {
users.push({ id: 1, name: 'Ada' });
expect(users).toHaveLength(1);
});
// Зелёный только потому, что тест выше выполнился первым и изменил `users`.
// Запустите этот тест в одиночку или перемешайте порядок — и он упадёт.
test('finds the created user', () => {
expect(users.find(u => u.name === 'Ada')).toBeDefined();
});
##Reasons for the Problem
Почему это происходит
- Общая фикстура (созданная один раз в
beforeAll, переменная уровня модуля,static-поле класса или реальная база данных/файл) изменяется тестами и никогда не сбрасывается между ними, поэтому каждый тест наследует остатки от предыдущего. - Удобство и скорость: разработчики переиспользуют дорогую подготовку или «достраивают» данные предыдущего теста, чтобы не создавать их заново (иногда это формализуется как цепочки тестов (Chained Tests)).
- Зависимость случайна и невидима — ничто в коде теста не объявляет «запусти меня после того теста», поэтому она сохраняется до тех пор, пока не изменится порядок.
Почему это вредно
- Надёжность / ложная уверенность. Набор тестов зелёный по счастливой случайности порядка. Измените порядок, распараллельте или запустите подмножество — и тесты упадут (или, что хуже, реальная регрессия скрывается, потому что более ранний тест случайно оставил «правильное» состояние). Тесты, зависящие от порядка, — одна из главных причин нестабильных (flaky) тестов.
- Сопровождаемость. Вы не можете запустить, отладить или перезапустить один упавший тест в изоляции — сначала должен выполниться тест-предпосылка. Добавление, удаление или перестановка тестов вызывает «жуткое действие на расстоянии».
- Читаемость. Тест больше не документирует одно поведение с явными входными данными; чтобы понять его, нужно прочитать всё, что выполнялось до него (таинственный гость (Mystery Guest), скрытый в порядке выполнения).
- Блокирует параллелизм и выборочный запуск. Шардирование тестов, параллельные раннеры и инструменты выбора/влияния тестов — все они предполагают независимость; связанность по порядку делает их небезопасными.
##Treatment
Сделайте каждый тест независимым: он сам настраивает всё необходимое, проверяет и убирает за собой, так что даёт одинаковый результат в любом порядке, в одиночку или в наборе.
- Дайте каждому тесту свежую фикстуру (Fresh Fixture). Перенесите общую подготовку из
beforeAllвbeforeEach(или создавайте её внутри теста), чтобы состояние пересоздавалось для каждого теста, а не накапливалось. - Устраните общее изменяемое состояние. Не читайте и не пишите переменные уровня модуля,
static-поля или глобальные переменные между тестами. Создавайте объекты локально; передавайте данные явно. - Сбрасывайте внешние ресурсы в teardown. Откатывайте БД (транзакция на тест) или используйте уникальные данные для каждого теста; удаляйте временные файлы; восстанавливайте переменные окружения, глобальные значения и поддельные таймеры; очищайте моки (
jest.clearAllMocks()/vi.restoreAllMocks(),jest.resetModules()). Предпочитайте автоматический/гарантированный teardown ручной очистке. - Докажите независимость, рандомизируя порядок. Это и есть настоящий детектор — это динамическое свойство, а не то, что может увидеть линтер:
- Jest:
--randomize/randomize: true. - Vitest:
sequence.shuffle(конфиг или--sequence.shuffle). - pytest:
pytest-randomlyилиpytest-random-order. - Maven Surefire:
-Dsurefire.runOrder=random. Запускайте также подмножество/одиночный тест в CI, чтобы выявить одинокие тесты (Lonely Tests).
- Jest:
- Если общая подготовка действительно нужна (дорогая фикстура только для чтения), сделайте её неизменяемой и общей только для чтения, либо в крайнем случае используйте намеренно задокументированный набор цепочечных тестов (Chained Test) — но никогда не случайную зависимость. Обратите внимание, что правило
no-hooksизeslint-plugin-jest/eslint-plugin-vitestможет отговорить от использования хуков setup/teardown, которые склонны провоцировать общее состояние, но само по себе оно не обнаруживает зависимость от порядка.
// До — зависит от порядка: тесту 2 нужны остатки от теста 1
let users = [];
test('creates a user', () => {
users.push({ id: 1, name: 'Ada' });
expect(users).toHaveLength(1);
});
test('finds the created user', () => {
expect(users.find(u => u.name === 'Ada')).toBeDefined();
});
// После — каждый тест владеет своей фикстурой
function makeRepo(seed = []) {
return { users: [...seed] };
}
test('creates a user', () => {
const repo = makeRepo();
repo.users.push({ id: 1, name: 'Ada' });
expect(repo.users).toHaveLength(1);
});
test('finds an existing user', () => {
const repo = makeRepo([{ id: 1, name: 'Ada' }]); // задаёт своё собственное предусловие
expect(repo.users.find(u => u.name === 'Ada')).toBeDefined();
});