Нестабильные тесты: четыре причины и их артефакты
Отнесите нестабильный тест к общей среде браузера, таймингу, сети или дрейфу окружения по артефакту, который их разделяет, и пусть повторы записывают, а не скрывают.
BotBrowser Team
Нужна структурированная документация по теме Документация?
Эта статья относится к редакционной библиотеке. Для пошаговой настройки, справки и постоянных обновлений переходите сразу в соответствующий раздел docs.
Нестабильный тест это не тест, который иногда падает; это тест, чей сбой ещё не отнесён к причине. Повтор до зелёного результата даёт зелёный прогон и уничтожает свидетельство того, какая из четырёх причин действовала. BotBrowser документирует изоляцию хранилища на запуск и на контекст, которая устраняет один механизм за причиной общего состояния, а не саму причину. Он не исправляет причины тайминга, сети или дизайна теста и не делает набор детерминированным сам по себе.
Кратко
- Записывайте первую неудачную попытку до любого повтора. Повтор, перезаписывающий её, уничтожил единственное свидетельство.
- Относите сбой к одной из четырёх причин: общее состояние браузера, тайминг, сеть или дрейф окружения. Каждая оставляет свой артефакт.
- Два сбоя с одной подписью это воспроизводимое наблюдение. Оно сужает причину, но не доказывает, что сбой не перемежаемый.
- Держите повторы как устройство записи. Записывайте каждую попытку, затем исправляйте причину и убирайте повтор из этого теста.
- BotBrowser устраняет один механизм общего состояния, изолируя хранилище по контексту или запуску. Остальные три причины он не исправляет.
Содержание
- Четыре причины и артефакт каждой
- Почему повтор стирает свидетельство
- Политика повторов, которая записывает
- Контрольный список изоляции состояния
- Практический вывод
Четыре причины и артефакт каждой
Первый полезный вопрос не «как часто падает», а «как выглядит сбой, когда он происходит». Каждая из четырёх причин оставляет подпись, и каждой подписи нужен свой артефакт для подтверждения. Артефакт выдвигает гипотезу; для её подтверждения нужно контрольное сравнение, а сбор неверного артефакта даёт уверенное и неверное отнесение.
| Причина | Подпись | Артефакт, который её подтверждает |
|---|---|---|
| Общее состояние | Падает только если до него шёл другой тест | Дамп хранилища, cookie и кэша перед сбойным шагом |
| Тайминг | Проходит с большим тайм-аутом, падает с обычным | Трасса с временами действий и ожидаемым событием |
| Сеть | Падает на том же запросе, а не на том же шаге | Журнал запросов со статусами и числом повторов |
| Дрейф окружения | Падает только на одном раннере, профиле или окне | Аргументы запуска, хеш профиля и окно для обоих прогонов |
Причина общего состояния хуже всего отражается в наборах с браузером, потому что артефакт невидим в сообщении о сбое. Практика гигиены тестовых данных и состояния браузера описывает, как не давать этому состоянию накапливаться. Тест, падающий только после другого теста, обычно падает на проверке содержимого, и сообщение указывает на проверку, а не на остатки хранилища. Дамп хранилища и cookie непосредственно перед сбойным шагом превращает это в отнесение.
Причина тайминга чаще всего «исправляется» без диагностики, а анализ ожиданий и стабильных тестов описывает, чего ждёт корректное ожидание. Тест, проходящий с большим тайм-аутом, показал, что ожидание было слишком коротким: это полезное свидетельство, а не ремонт. Трасса показывает, появился ли элемент поздно, была ли медленной сеть или действие сработало до стабилизации страницы, и эти три ответа ведут к разным исправлениям.
Причины сети и тайминга легко спутать, потому что обе выглядят как «элемента не было». Различие в том, что сетевая причина воспроизводится на том же запросе, а причина тайминга на том же шаге с другим запросом. Запись статуса запроса и числа повторов разделяет их без догадок.
Причина окружения выглядит как ошибка фреймворка. Тест, падающий на одном раннере и проходящий на другом, обычно падает потому, что у прогонов были разные аргументы запуска, значения профиля или окно. Запись всех трёх для обоих прогонов дешева, и это артефакт, который делает сравнение возможным.
Пятая подпись заслуживает заметки, потому что её часто читают как одну из четырёх: тест зависит от данных, меняющихся со временем. Проверка цены против литерала, проверка даты, предполагающая сегодня, или проверка списка с фиксированным количеством падают по календарю, а не перемежаемо. Артефакт это входные данные и их отметка времени, а исправление принадлежит фикстуре, а не браузеру. Трактовка как причины состояния или тайминга ведёт к повторам, которые проходят в одни дни и падают в другие, самый дорогой исход.
Порядок расследования важен не меньше таблицы. Начинайте с причины, которую дешевле всего исключить и исправить, почти всегда это состояние, потому что новый контекст либо воспроизводит сбой, либо нет. Затем проверьте окружение, потому что сравнение двух записанных конфигураций запуска это diff, а не эксперимент. Тайминг и сеть идут последними, потому что диагностика любого требует трассы и журнала запросов, которые стоят реального времени.
Есть и гигиенический пункт, применимый ко всем четырём. Артефакт полезен, только если записан в момент сбоя, а не восстановлен позже. Снимок, сделанный после того как фреймворк уже ушёл со страницы, дамп после хука очистки или журнал, отфильтрованный только к ошибкам, будут выглядеть артефактами, не содержа свидетельства. Записывайте в хуке сбоя, до любого teardown, и фиксируйте, какой хук создал артефакт.
Почему повтор стирает свидетельство
Playwright документирует повторы как функцию раннера, которая перезапускает упавшие тесты, а фикстуры и параллельное выполнение как отдельные функции со своими правилами области. Ни один из этих документов не утверждает, что повторы делают набор надёжным. Функция перезапускает тест; что происходит со свидетельством первой попытки, полностью зависит от того, как набор настроен его записывать.
Режим отказа по умолчанию тонок, и он виден в одном поле: status=passed при retries=1. Playwright сообщает повторённый тест, который в итоге прошёл, как flaky, а testInfo.status при этом равен passed, поэтому отчёт, читающий только поле итогового статуса, видит прохождение и больше ничего. Более серьёзная проблема в том, что у первой попытки часто вообще нет трассы: значение по умолчанию trace: 'off' ничего не записывает, а on-first-retry начинает запись только с началом повтора. Артефакт отсутствует, а не перезаписан, если только пользовательский путь вывода не переиспользует одно имя файла между попытками.
Поэтому повтор это устройство записи, а не исправление. При хорошем использовании он говорит, какие тесты нестабильны, и даёт второе наблюдение того же сбоя. При плохом он превращает реальный дефект в зелёную сборку и переносит обнаружение в продакшен, где артефакта уже нет.
Есть и статистическая ловушка. Тест, падающий один раз из десяти, пройдёт один повтор девять раз из десяти, поэтому набор с повторами выглядит гораздо стабильнее, чем он есть. Частота сбоев не изменилась; изменилась только отчётность. Два сбоя подряд с одной подписью это самый сильный доступный сигнал, но это всё ещё гипотеза: сетевая причина может дважды дать одну подпись и остаться перемежаемой, поэтому отнесению нужен контрольный опыт.
Политика повторов, которая записывает
Политика, которая записывает, невелика: хранить каждую попытку, называть артефакт по попытке и отказываться сводить первый сбой к нулю. Конструктор ниже это сконструированный пример того, что должна содержать запись.
// Constructed fixture: three attempts of one test, two failures with a matching artifact prefix.
const attempts = [
{ n: 1, status: 'failed', artifact: 'storage-dump:t1.json', at: '2026-11-10T09:00:12Z' },
{ n: 2, status: 'failed', artifact: 'storage-dump:t2.json', at: '2026-11-10T09:00:48Z' },
{ n: 3, status: 'passed', artifact: 'trace:t3.zip', at: '2026-11-10T09:01:30Z' },
];
const failures = attempts.filter(a => a.status === 'failed');
const sameArtifact = failures.length > 1 && failures[0].artifact.split(':')[0] === failures[1].artifact.split(':')[0];
const verdict = sameArtifact ? 'repeated_signature' : failures.length ? 'single_failure' : 'clean';
const lines = [
`attempts=${attempts.length} failures=${failures.length}`,
...attempts.map(a => `attempt=${a.n} status=${a.status} artifact=${a.artifact} at=${a.at}`),
`verdict=${verdict}`,
];
console.log(lines.join('\n'));
Сконструированный вывод и есть суть: третья попытка прошла, а запись всё равно говорит, что подпись повторилась.
attempts=3 failures=2
attempt=1 status=failed artifact=storage-dump:t1.json at=2026-11-10T09:00:12Z
attempt=2 status=failed artifact=storage-dump:t2.json at=2026-11-10T09:00:48Z
attempt=3 status=passed artifact=trace:t3.zip at=2026-11-10T09:01:30Z
verdict=repeated_signature
Три правила делают политику рабочей. Называйте артефакт по причине, а не по файлу, чтобы storage-dump и trace различались с первого взгляда. Держите попытки по порядку с их временем, чтобы рецензент видел, был ли сбой немедленным или запоздалым. И делайте вердикт производным полем, а не человеческим суждением, чтобы отчёт не мог тихо назвать повторяющийся сбой нестабильностью.
Ещё два правила относятся к отчёту, а не к конструктору. Во-первых, публикуйте число сбоев рядом с числом прохождений, потому что набор, сообщающий только итоговые статусы, скрывает сбои, устранённые повтором. Во-вторых, храните артефакт каждой попытки, а не только последней, потому что первая попытка шла до любой очистки, привязанной к повтору, а набор, сбрасывающий состояние в хуке повтора, уже изменил условия, которые породили сбой.
Область повтора это третье соображение. Повтор на уровне теста даёт повторные наблюдения одного теста; повтор на уровне файла перезапускает только упавшие тесты, а повтор, настроенный для последовательной группы, перезапускает всю группу, включая тесты, которые не падали, что меняет, какие тесты делят состояние, и может сделать вторую попытку другим экспериментом. Playwright также документирует повтор по группе или по последовательной группе, где все тесты группы повторяются вместе, поэтому вторая попытка выполняет другой набор тестов. Предпочитайте самую узкую область, воспроизводящую сбой, и записывайте область в записи попытки, чтобы поздний читатель знал, что именно перезапускалось.
Политике повторов нужен и владелец, и условие выхода. Владелец это тот, кто может исправить причину, а условие выхода это дата, к которой повтор должен быть убран или тест переведён в карантин. Без обоих повтор, добавленный во время инцидента, становится постоянной инфраструктурой, которую никто не пересматривает, и набор постепенно заполняется тестами с неизвестным реальным статусом.
Карантин это честная альтернатива бесконечным повторам. Вынос заведомо сломанного теста из обязательного набора с приложенным артефактом и названным владельцем сохраняет сигнал набора, пока причина расследуется. Это видимый сбой, а не скрытый, и видимые сбои исправляют.
Политике нужна и стоп-правило. Если две попытки упали с одной подписью, прекратите повторять и измените одну переменную: подпись воспроизводима, поэтому ещё одна попытка в той же конфигурации не добавит информации. Следующий шаг это контрольное сравнение из таблицы, а не третий повтор. Продолжение после этой точки тратит время на производство уже существующего свидетельства и толкает набор к состоянию «зелено за счёт повторов», которое политика и должна предотвращать.
Контрольный список изоляции состояния
Причину состояния стоит проверить первой, потому что её дешевле всего устранить и легче всего спутать с чем-то другим. Каждый пункт даёт наблюдение, а не веру, поэтому список можно пройти до диагностики остальных трёх причин.
| Проверка | Наблюдение, которое она даёт |
|---|---|
| Новый контекст на тест | Выживает ли сбой в пустом контексте |
| Хранилище очищено до навигации | Имело ли значение остаточное локальное хранилище |
| Cookie очищены, а не просто истекли | Изменил ли результат сохранённый cookie |
| Нет общего профиля между воркерами | Писали ли два воркера в один профиль |
| Фиксированные окно и значения устройства | Изменили ли результат различия вёрстки |
| Записанные аргументы запуска | Были ли два прогона настроены одинаково |
Модель фикстур и изоляции описывает область, которая делает новый контекст на тест практичным. Первая строка решающая. Если тест падает в общем контексте и проходит в новом, причина это состояние, и никакая правка тайм-аута её не устранит. Если падает в обоих, причина состояния маловероятна и следующие две заслуживают усилий.
Четвёртая строка становится срочной при параллельном выполнении. Playwright документирует параллельное выполнение как тесты в отдельных воркерах, и воркер, переиспользующий один профиль между тестами, может оставить состояние следующему тесту этого воркера, оставив другие чистыми. Результат это картина сбоев, выглядящая случайной по числу воркеров, а не по тесту.
Переиспользование воркера даёт второй, более тонкий сигнал: сбой перемещается. Если тот же тест проходит последовательно и падает только параллельно, и падающая проверка меняется между прогонами, причина почти наверняка общее состояние, пересекающее границу воркера. Запись идентификатора воркера с каждой попыткой превращает это из подозрения в картину, потому что упавшие попытки сгруппируются на одном воркере.
Очистка хранилища это не то же, что очистка cookie, и ни одно из них не равно запуску нового контекста. Локальное хранилище, хранилище сессии, IndexedDB, кэш сервис-воркера и HTTP-кэш переживают разные сбросы, и тест, зависящий от одного из них, продолжит падать после очистки остальных. Самый дешёвый полный сброс это новый контекст браузера на тест, и это же наблюдение даёт первая строка списка.
Сервис-воркер заслуживает отдельной строки, потому что переживает перезагрузку и может отдавать устаревший ответ спустя долго после того, как тест считает хранилище очищенным. Тест, падающий в первом прогоне после развёртывания и проходящий потом, часто читает кэшированный ответ воркера, зарегистрированного предыдущим тестом. Запись, был ли сервис-воркер зарегистрирован в момент сбоя, это артефакт, отделяющий это от сетевой причины.
Последний пункт списка делает остальные пять воспроизводимыми. Два прогона с разными аргументами это не один эксперимент, и сбой, появляющийся только в одном из них, может быть разницей конфигурации, а не кода. Запись аргументов, хеша профиля и окна вместе даёт рецензенту одну сравнимую строку, а сравнение двух таких строк это diff, а не спор.
Документация BotBrowser по управлению профилями документирует, что каждый запуск использует свой user-data-dir, cookie и настройки сессии, а внутри одного процесса хранилище, cookie и настройки контекста изолируются BrowserContext. Документация по конфигурации профилей перечисляет входные данные окружения, которые несёт профиль: локаль, часовой пояс, экран и WebGL. Вместе они устраняют один механизм состояния между тестами: новый контекст или запуск не может унаследовать хранилище предыдущего теста. Это уже, чем устранение причины состояния вообще, и ничего не говорит о тайминге, сети или дизайне теста, поэтому набор с проблемой тайминга останется нестабильным даже с идеальным профилем.
Вопросы и ответы
Тест, проходящий при повторе, является нестабильным?
Это тест с неотнесённым сбоем. Повтор изменил результат, а не причину. Сохраните артефакт первой попытки и отнесите сбой, прежде чем считать тест здоровым.
Можно ли убрать нестабильность увеличением тайм-аутов?
Только если причина это тайминг, и только после того как трасса покажет, что ожидание было ограничивающим фактором. Увеличение тайм-аута при причине состояния или сети скрывает сбой дольше, не меняя его.
Почему тест падает только на одном раннере?
Эта подпись указывает на дрейф окружения: разные аргументы запуска, профиль или окно. Запишите все три для обоих прогонов и сравните их, прежде чем искать ошибку фреймворка.
Делает ли BotBrowser нестабильный набор детерминированным?
Нет. Изоляция касается хранилища: она снимает остаточные данные предыдущего теста, но гонки тайминга, сбои сети и ошибки в дизайне теста остаются, поэтому никакая настройка браузера не делает набор детерминированным.
Источники
- Повторы тестов в Playwright
- Фикстуры Playwright
- Параллельное выполнение Playwright
- Управление профилями BotBrowser
- Конфигурация профилей BotBrowser
- Согласованность автоматизации в BotBrowser
Практический вывод
Записывайте первую неудачную попытку до любого повтора, относите сбой к состоянию, таймингу, сети или окружению по артефакту, который их разделяет, и только затем меняйте одну вещь. Держите повторы как устройство записи: каждая попытка со своим артефактом и временем, производный вердикт и стоп-правило, когда две попытки падают одинаково. Проверяйте механизм общего состояния первым, потому что его дешевле всего проверить, и помните, что изоляция хранилища по контексту касается только этого механизма.
Похожие статьи
Selenium Grid: почему два узла расходятся
СравнениеЗащита движка и API: воспроизводимое сравнительное тестирование
Начало работыЖизненный цикл BrowserContext и состояние хранилища в Playwright
ОтпечаткиЧто на самом деле проверяют публичные наборы тестов
Начало работыНадёжные тесты загрузки и скачивания файлов в браузерной автоматизации
ИдентичностьГигиена тестовых данных и состояния браузера в автоматизации
Выполните ту же проверку в BotBrowser
Загрузите профиль, сохраните внутри браузера те сигналы, о которых только что прочитали, и подтвердите результат на своей машине до раскатки.