8 lines
18 KiB
JSON
8 lines
18 KiB
JSON
{
|
||
"index": 157,
|
||
"slug": "editorial-2023-08-field-e2e-stability",
|
||
"title": "Flaky e2e-тест: как найти причину и сохранить сигнал",
|
||
"excerpt": "Если первый запуск падает, а retry проходит, тест не становится надёжным. Разбираем locator, готовность интерфейса, данные и trace, чтобы менять только доказанный слой.",
|
||
"contentHtml": "<p>В CI тест оформления платежа падает на <code>click</code>. Повторный запуск проходит. Через день тот же тест снова красный, но уже на проверке результата. Команда увеличивает timeout, добавляет ещё один retry и получает более длинную очередь. Ошибка не исчезает: тест лишь дольше скрывает нарушение пользовательского сценария.</p>\n<p>Цена такого решения измерима. Разработчик ждёт обратную связь дольше. Красный запуск перестаёт отличать дефект продукта от дефекта теста. Если retry маскирует настоящий сбой оплаты, команда может пропустить проблему до релиза. Если виноваты общие данные, каждый тест с большим timeout платит за чужую гонку.</p>\n<p>Тезис простой: flaky — это сигнал для разбора, а не причина менять настройки вслепую. Сначала разделите locator, actionability, readiness, данные и внешние зависимости. Затем привяжите evidence к конкретной попытке. После этого выбирайте маленькую правку с owner и понятным rollback.</p>\n<h2>Что происходит между двумя попытками</h2>\n<p>Статус <code>failed → passed</code> сообщает только о разных исходах запусков. Он не называет причину. Retry не продолжает ту же страницу с того же места. Runner создаёт новую попытку и может использовать другой worker. Меняются cookies, storage, порядок тестов, состояние базы, очистка данных и доступность внешнего сервиса.</p>\n<p>У действия есть несколько независимых условий. Locator должен указывать ровно на один элемент. Перед <code>click</code> элемент должен быть видимым, стабильным, доступным для событий и активным. После действия интерфейс должен показать пользовательский результат. Успешный click доказывает готовность действия. Он не доказывает, что платёж подтверждён.</p>\n<p>Ожидание <code>networkidle</code> не равно готовому экрану. Исчезнувший spinner не равен успешной операции. Прошедший retry не равен воспроизводимому тесту. Trace помогает увидеть ход одной попытки, но не подменяет контракт результата.</p>\n<h2>Минимальный контракт теста</h2>\n<p>Зафиксируйте четыре факта до изменения конфигурации: чем пользователь находит control, какой результат он должен увидеть, что произошло в initial и retry, и какой артефакт относится к каждой попытке. Не называйте доказательством файл без номера попытки. Trace retry не объясняет автоматически initial failure.</p>\n<pre><code>import { expect, test } from '@playwright/test'; test('confirms payment', async ({ page }) => { await page.goto('/checkout'); await page.getByRole('button', { name: 'Оплатить' }).click(); await expect(page.getByRole('status')).toHaveText('Платёж подтверждён'); }); // Учебный пример: текст, маршрут и данные замените на контракт конкретного приложения.</code></pre>\n<p>В примере locator описывает действие языком пользователя. Assertion ждёт смысловой результат, а не случайную задержку. Это учебный фрагмент: он не подтверждает работу платёжного контура и не заменяет проверку в вашем окружении. В реальном тесте задайте независимые данные и очистку, иначе зелёный запуск может зависеть от предыдущего теста.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><caption>Как сузить область исправления</caption><thead><tr><th scope='col'>Симптом</th><th scope='col'>Причина</th><th scope='col'>Проверка</th><th scope='col'>Действие</th></tr></thead><tbody><tr><td>Ноль или несколько совпадений locator</td><td>Selector не описывает одну цель</td><td>Проверьте роль, имя и область контейнера</td><td>Исправьте locator; timeout не меняйте</td></tr><tr><td>Click ждёт и завершается timeout</td><td>Элемент скрыт, перекрыт, движется или disabled</td><td>Смотрите actionability и состояние экрана</td><td>Исправьте предусловие или selector</td></tr><tr><td>Click прошёл, результата нет</td><td>Неверный readiness contract, данные или ответ сервиса</td><td>Сверьте assertion с пользовательским итогом и входом</td><td>Уточните UI-контракт или изоляцию данных</td></tr><tr><td>Initial failed, retry passed</td><td>Гонка, загрязнение данных или внешний сбой</td><td>Сравните worker, данные, порядок и evidence</td><td>Создайте triage; не добавляйте retry автоматически</td></tr><tr><td>Trace есть только у retry</td><td>Контекст попыток собран несимметрично</td><td>Проверьте attempt, тест и проект в отчёте</td><td>Добавьте путь сбора initial evidence</td></tr></tbody></table>\n<h2>Locator и readiness проверяйте отдельно</h2>\n<p>Начните с cardinality. Locator должен находить ровно один элемент в момент действия. Если кнопок несколько, сузьте область диалога или списка. Если совпадений нет, разберитесь с состоянием страницы и текстом. Увеличение timeout не делает неоднозначную цель однозначной.</p>\n<p>Предпочитайте роль, доступное имя и label. CSS-цепочка по классам связывает тест со строением DOM, а не с поведением интерфейса. Это не абсолютный запрет на test id или CSS. Test id полезен для стабильного технического контракта. CSS оправдан, когда команда сознательно поддерживает его как API. Важно назвать владельца и смысл locator.</p>\n<p>После click проверяйте факт, который видит пользователь: сообщение об успехе, новую запись, смену статуса или подтверждённый маршрут. Не используйте spinner как финальный результат. Не делайте <code>expect(await locator.isVisible()).toBe(true)</code>, если нужен web-first assertion: такая форма сначала получает снимок состояния и теряет встроенное ожидание.</p>\n<h2>Retry должен сохранять сигнал</h2>\n<p>Retry полезен как диагностический слой и как защита от краткого сбоя инфраструктуры. Он опасен, когда превращается в разрешение на merge. Запишите номер попытки, worker, используемые данные и исходную ошибку. Слово «flaky» описывает классификацию запусков. Оно не заменяет root cause.</p>\n<p>Не смешивайте классы причин. Ошибка locator требует проверки DOM. Ошибка actionability требует проверки overlay, animation и disabled state. Отсутствие readiness требует проверки UI и ответа операции. Разные данные требуют проверки setup и cleanup. Внешний сервис требует отдельной политики зависимости. Один глобальный timeout не лечит все случаи.</p>\n<pre><code>use: { trace: 'on-first-retry', screenshot: 'only-on-failure' }, retries: process.env.CI ? 1 : 0 // Учебная конфигурация. Значения зависят от цены очереди и среды.</code></pre>\n<p>Фрагмент показывает форму, а не готовую политику. Trace на первом retry даёт контекст повторной попытки и экономит место. Он не создаёт trace initial failure. Если первичный контекст критичен, настройте отдельный способ его сохранить и явно подпишите артефакт.</p>\n<h2>Trace отвечает на узкий вопрос</h2>\n<p>Откройте trace с одним вопросом: locator указывал на одну кнопку перед click или после click появился нужный status? Trace Viewer позволяет сопоставить timeline, DOM snapshot, action log и сетевые запросы. Это помогает сузить гипотезу. Но trace показывает конкретный запуск. Он не знает, был ли результат бизнес-успешным, пока тест не проверяет assertion.</p>\n<p>Если артефакта нет, запишите <code>evidence: absent</code>. Не заменяйте отсутствие данных уверенным объяснением. Сначала сверяйте имя теста, проект, commit и номер попытки. Затем смотрите один слой. Широкий запрос «найти причину по trace» часто приводит к непроверенной версии.</p>\n<figure><img src='/assets/editorial/2023/e2e-stability-2023-triage-loop.svg' alt='Цикл разбора flaky e2e-теста: сравнение initial и retry, проверка locator и readiness, затем малая правка с rollback' loading='lazy' /><figcaption>Схема задаёт порядок разбора: сначала фиксируются две попытки, затем отдельно проверяются цель действия и пользовательский результат. Это иллюстрация процедуры, а не реальный trace или отчёт CI.</figcaption></figure>\n<h2>Порядок действий</h2>\n<ol><li><strong>Зафиксируйте симптом.</strong> Сохраните initial error, retry outcome, номер попытки и ссылку на отчёт. Не сокращайте запись до «иногда падает».</li><li><strong>Назовите причины.</strong> Разделите locator, actionability, readiness, изоляцию данных и внешнюю зависимость.</li><li><strong>Проверьте locator.</strong> Убедитесь, что он находит ровно один пользовательский control в нужной области.</li><li><strong>Проверьте readiness.</strong> Сопоставьте assertion с финальным фактом интерфейса. Уберите sleep и ожидание вторичного сигнала.</li><li><strong>Сравните попытки.</strong> Проверьте worker, cookies, storage, входные данные, cleanup и порядок запуска.</li><li><strong>Привяжите evidence.</strong> Свяжите trace, screenshot или log с attempt и задайте артефакту один вопрос.</li><li><strong>Сделайте малый diff.</strong> Меняйте только подтверждённый слой: locator, assertion, данные или политику артефактов.</li><li><strong>Опишите rollback.</strong> Укажите владельца, что вернуть, каким запуском проверить результат и когда снять временное исключение.</li></ol>\n<h2>Проверьте отрицательный путь</h2>\n<p>Проверяйте не только успешную оплату. Добавьте сценарий, в котором сервер возвращает отказ или данные невалидны. Убедитесь, что тест видит сообщение об ошибке и не принимает disabled control, spinner или старый status за успех. Если отрицательный путь ломается из-за случайного текста, проблема может быть в контракте интерфейса, а не в retry.</p>\n<p>Проверка изоляции тоже должна иметь отрицательный путь. Запустите тест отдельно и в другом порядке. Используйте новый идентификатор данных. Удалите запись после сценария. Если результат меняется, не маскируйте гонку timeout. Найдите владельца состояния и границу cleanup.</p>\n<h2>Ограничения</h2>\n<p>Ни один locator не защищает от неверного продукта. Auto-waiting ждёт actionability, но не исправляет серверный ответ. Web-first assertion ждёт условие, но не делает условие правильным. Retry может уменьшить шум инфраструктуры, но может и скрыть редкую ошибку. Trace полезен только там, где его записали и правильно связали с попыткой.</p>\n<p>Учебные фрагменты не дают production-результатов. Они не измеряют flake rate, длительность очереди, совместимость браузеров или качество данных. Версию Playwright, project config и окружение нужно сверять отдельно. Повышение timeout допустимо только после доказанной верхней границы задержки и проверки, что ожидание относится к нужному событию.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Разбор готов, если команда может показать карточку запуска и ответить на пять вопросов: что упало в initial, что прошло в retry, какой locator использовался, какой пользовательский результат ожидался и какой evidence относится к каждой попытке. После правки отдельный запуск проверяет тот же результат на свежих данных, а отрицательный путь завершается ожидаемой ошибкой.</p>\n<p>Для временного исключения нужны owner, срок пересмотра и условие снятия. Для постоянной правки нужны малый diff и понятный rollback. Один зелёный retry не проходит этот критерий. Проходит повторяемый контракт, в котором тест отличает готовое действие от подтверждённого результата.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href='https://playwright.dev/docs/best-practices' target='_blank' rel='noopener noreferrer'>Playwright: Best Practices</a> — официальные рекомендации по пользовательским locator, web-first assertions и trace для CI.</li><li><a href='https://playwright.dev/docs/actionability' target='_blank' rel='noopener noreferrer'>Playwright: Auto-waiting</a> — официальное описание проверок actionability перед действиями.</li><li><a href='https://playwright.dev/docs/test-retries' target='_blank' rel='noopener noreferrer'>Playwright: Retries</a> — официальное описание повторных запусков и классификации результата.</li></ul>"
|
||
}
|