---
title: "Война тестовых прогонов"
type: "test-smell"
slug: "test-run-war"
url: "http://localhost:3000/ru/test-smells/test-run-war.md"
category: "Запахи нестабильности"
description: "Война тестовых прогонов — это когда тесты проходят у одного человека, но начинают случайно падать, стоит нескольким людям или CI-задачам запустить набор тестов одновременно, потому что тесты конфликтуют на общей персистентной фикстуре."
---
# Война тестовых прогонов

> Война тестовых прогонов — это когда тесты проходят у одного человека, но начинают случайно падать, стоит нескольким людям или CI-задачам запустить набор тестов одновременно, потому что тесты конфликтуют на общей персистентной фикстуре.

## Signs and Symptoms

Войну тестовых прогонов узнают по тому, _кто_ и _когда_, а не по самому телу теста:

* Набор тестов зелёный на вашей машине и в одиночных прогонах CI, но краснеет «без причины», когда коллега запускает его одновременно или когда две CI-задачи/конвейера обращаются к одному окружению.
* Падения **преходящи и невоспроизводимы** — повторный запуск того же теста обычно делает его зелёным.
* Падения группируются вокруг общего, изменяемого, персистентного ресурса: одна общая тестовая база, общий S3-бакет/очередь/кеш или фиксированный аккаунт/тенант.
* Формы ошибок характерны: нарушения уникальности / дублирующегося ключа, `expected 1 row but found 2` или `record not found` (чужой прогон её удалил).

```js
// Оба теста считают, что монопольно владеют ОБЩЕЙ тестовой базой данных.
test('creates a user', async () => {
  await db.users.insert({ id: 42, email: 'alice@example.com' }); // жёстко зашитый ключ
  expect(await db.users.findById(42)).toMatchObject({ email: 'alice@example.com' });
});

test('counts active users', async () => {
  expect(await db.users.countActive()).toBe(1); // предполагает, что никто другой не добавлял строк
});

```

Когда два раннера обращаются к одной базе одновременно, вставка сталкивается на `id: 42` (дублирующийся ключ), а глобальный счётчик уже не равен `1`. По отдельности каждый тест проходит; вместе и одновременно — они дерутся.

## Reasons for the Problem

**Почему это происходит.** Тесты опираются на глобальную **общую фикстуру (Shared Fixture)** — обычно единственный персистентный ресурс (одна общая тестовая БД, общий бакет, фиксированный пользователь) вместо **свежей фикстуры (Fresh Fixture)**, создаваемой на каждый тест. Жёстко зашитые идентификаторы и проверки глобального состояния молча предполагают, что тест _владеет_ ресурсом. Пока в каждый момент к нему обращается ровно один прогон, иллюзия держится. Добавьте конкурентность — второго разработчика, параллельные CI-задачи или параллельные тестовые воркеры — и прогоны начинают переплетаться на одних и тех же строках и ключах.

**Чем это вредит.** Месарош относит это к разновидности **непостоянного теста (Erratic Test)**: по сути это проблема взаимодействующих тестов (Interacting Tests), где взаимодействие происходит _между конкурентными прогонами тестов_, а не между тестами в рамках одного прогона.

* **Надёжность:** падения недетерминированы и зависят от того, кто ещё запускается, поэтому их нельзя воспроизвести по требованию.
* **Ложная уверенность и утраченное доверие:** команда привыкает «просто перезапустить» и начинает отмахиваться от красных сборок — включая настоящие регрессии, прячущиеся в шуме.
* **Сопровождаемость / отлаживаемость:** причина невидима в упавшем тесте; она живёт в _другом_ прогоне, а переплетение делает поиск первопричины мучительным.
* **Масштабируемость:** это блокирует ровно то, чего команды хотят больше всего, — распараллеливание набора тестов и рост числа людей, способных запускать его одновременно.

## Treatment

Устраните общее, оспариваемое состояние. Конкретные шаги, примерно в порядке предпочтения:

1. **Предпочитайте свежую фикстуру (Fresh Fixture).** Каждый тест создаёт ровно нужные ему данные и убирает их за собой — или выполняется внутри транзакции, откатываемой при завершении. Не зависьте от заранее существующих общих строк.
2. **Изолируйте на каждый раннер (песочница БД / партиционирование).** Дайте каждому разработчику _и_ каждой CI-задаче/воркеру собственную базу, схему или пространство имён (схема-на-воркер, эфемерная контейнерная БД или branch-база). Тогда конкурентные прогоны просто не видят друг друга.
3. **Используйте различимые сгенерированные значения (Distinct Generated Values) для ключей.** Замените жёстко зашитые id/email уникальными сгенерированными значениями (UUID, последовательность или `timestamp + workerId`), чтобы конкурентные прогоны создавали _различные_ объекты, а не сталкивались.
4. **Не проверяйте глобальные счётчики.** Ограничивайте проверки данными, которые создал именно этот тест (фильтруйте по его уникальному ключу/тенанту), вместо `count == 1`.
5. **Делайте внешние ресурсы на каждый тест/воркер.** Уникальные временные каталоги, уникальные имена очередей/топиков, БД в памяти или testcontainer на каждый процесс.

```js
// ДО: жёстко зашитый ключ + глобальная проверка против общей БД
await db.users.insert({ id: 42, email: 'alice@example.com' });
expect(await db.users.countActive()).toBe(1);

```

```js
// ПОСЛЕ: уникальный ключ + узкая проверка (Fresh Fixture + Distinct Generated Values)
const id = crypto.randomUUID();
const email = `alice+${id}@example.com`;
await db.users.insert({ id, email });
expect(await db.users.findById(id)).toMatchObject({ email });
// ...и направьте каждый воркер на собственную схему/песочную базу

```

Примечание: линтера для этого нет — оно проявляется только во время выполнения при конкурентности. Сделайте его воспроизводимым, намеренно запуская набор тестов дважды параллельно против одного окружения (или увеличив число воркеров раннера) в CI; если после этого набор краснеет — у вас есть война тестовых прогонов, которую нужно чинить.
