8 lines
20 KiB
JSON
8 lines
20 KiB
JSON
{
|
||
"index": 159,
|
||
"slug": "editorial-2023-08-practice-e2e-stability",
|
||
"title": "E2E без лишних retry: ждать факт, а не тишину интерфейса",
|
||
"excerpt": "Почему retry не лечит e2e-падение: отделяем locator, условие готовности, повторный запуск и evidence, а timeout меняем только после проверки контракта.",
|
||
"contentHtml": "<p>Тест нажимает кнопку «Оплатить», получает ошибку на первой попытке и проходит на повторной. В отчёте остаётся статус flaky, а в pull request появляется короткое предложение: поднять timeout и идти дальше. Проблема в том, что этим действием смешиваются четыре разных объекта: selector для действия, условие готовности интерфейса, retry тест-раннера и evidence из конкретного запуска. Пока они смешаны, команда лечит паузу, а не контракт.</p>\n<p>Цена такой правки не сводится к лишним секундам CI. Реальная ошибка может стать «шумом»: повторный запуск проходит, скрывает первый отказ и откладывает разбор. Обратная цена тоже заметна: бесконечный trace на каждый тест раздувает артефакты, но не отвечает, какой результат должен увидеть пользователь. Здесь нужен короткий порядок: сначала назвать факт после действия, затем выбрать наблюдение, только потом решать, нужен ли retry и какой evidence сохранять.</p>\n<h2>Четыре объекта, которые нельзя называть одним ожиданием</h2>\n<p>Selector отвечает на вопрос «куда направить действие». Для Playwright 1.37.0 locator с role и name — это способ найти пользовательский элемент. Перед <code>click()</code> Playwright проверяет, что элемент attached, visible, stable, receives events и enabled. Эти проверки полезны: они не дают кликнуть в скрытый или перекрытый control. Но они не знают, завершилась ли оплата, сохранился ли профиль или пришёл ли пользовательский статус.</p>\n<p>Readiness condition отвечает на другой вопрос: какой наблюдаемый продуктовый факт должен появиться после действия. Это может быть текст в <code>role=status</code>, появление записи в таблице или смена доступного пользователю состояния. Условие не должно быть «страница немного успокоилась» или «кнопка стала disabled», если бизнес-операция ещё не подтверждена. Retry запускает тест повторно после failure. Evidence — trace, snapshot, action log или другой артефакт отдельной попытки. Ни один из них не является синонимом selector или готовности.</p>\n<div class=\"table-scroll\"><table><caption>Граница каждого слоя в e2e-сценарии</caption><thead><tr><th scope=\"col\">Слой</th><th scope=\"col\">На какой вопрос отвечает</th><th scope=\"col\">Что может подтвердить</th><th scope=\"col\">Чего не подтверждает</th><th scope=\"col\">Первое действие</th></tr></thead><tbody><tr><td>Selector</td><td>Как найти control?</td><td>locator разрешается ровно в один ожидаемый элемент</td><td>что операция завершилась</td><td>использовать role/name или явный test id</td></tr><tr><td>Readiness condition</td><td>Какой факт увидел пользователь после действия?</td><td>конкретный status, текст, запись или состояние</td><td>что locator был уникален до click</td><td>сформулировать ожидаемый продуктовый результат</td></tr><tr><td>Retry</td><td>Что сделал runner после failure?</td><td>первая попытка не прошла, следующая прошла или нет</td><td>почему попытки различаются</td><td>сохранить статус первой и повторной попытки</td></tr><tr><td>Evidence</td><td>Что можно изучить в одном запуске?</td><td>действия, snapshots, log и network log, если они записаны</td><td>корневую причину без дополнительной проверки</td><td>связать артефакт с номером попытки</td></tr><tr><td>Timeout</td><td>Каков верхний предел ожидания?</td><td>когда ожидание остановится ошибкой</td><td>какой факт надо дождаться</td><td>не менять, пока не назван readiness contract</td></tr></tbody></table></div>\n<h2>Сначала формулируем результат после click</h2>\n<p>Практический контракт выглядит короче, чем кажется. После нажатия нужно назвать одно состояние, которое экран обязан показать. Например: «платёж подтверждён», а не «кнопка уже недоступна». Если интерфейс не имеет такого состояния, это не повод ждать произвольную паузу. Это повод вместе с владельцем экрана выбрать доступный сигнал: status region, итоговую строку, новый route или ответственный элемент в UI. Контракт должен быть проверяемым пользователем, а не внутренним CSS-классом без смысла вне реализации.</p>\n<p>Ниже — проектный образец. <code>getByRole()</code> выбирает кнопку, а <code>toHaveText()</code> ждёт текст результата. Web-first assertion в Playwright умеет повторять проверку до timeout; это не означает, что любой текст подходит. Значение <code>confirmed</code> здесь специально условное: в своём продукте его заменяют на реальный, устойчивый и доступный пользователю результат. Фрагмент не доказывает, что страница, backend или browser уже проверены.</p>\n<pre><code>// Иллюстрация контракта: locator выбирает действие, expect проверяет результат.\nimport { expect, test } from '@playwright/test';\n\ntest('подтверждает оплату', async ({ page }) => {\n await page.getByRole('button', { name: 'Оплатить' }).click();\n await expect(page.getByTestId('payment-state')).toHaveText('confirmed');\n});\n\n// getByRole — selector. toHaveText — readiness condition.\n// Текст и test id должны соответствовать контракту конкретного продукта.</code></pre>\n<p>Sleep или network idle без связи с результатом делают запуск длиннее, но не объясняют, что делать при ошибке экрана после успешного запроса. Actionability делает действие допустимым, readiness делает завершение сценария наблюдаемым. Тогда timeout — параметр договора, а не способ спрятать расхождение.</p>\n<h2>Учебный fixture: только synthetic попытки в памяти</h2>\n<p>Пакет содержит исполнимую модель, но не e2e-запуск. Она создаёт две synthetic попытки: первая не достигает <code>payment-state=confirmed</code>, вторая достигает его после retry. Selector в обеих уникален, поэтому fixture отделяет readiness от selector и retry. Он не открывает URL, не читает тест, не запускает Playwright и не производит <code>trace.zip</code> или video.</p>\n<pre><code>node web/scripts/upgrade-2023-08.mjs --verify-fixture\n\n# Команда детерминированно создаёт marked synthetic attempts только в памяти.\n# Она не открывает браузер, не читает проект, не запускает Playwright,\n# не записывает trace/video, не измеряет duration и не проверяет compatibility.\n# PASS проверяет разделение selector, readiness, retry и synthetic evidence.\n# PASS не означает, что настоящий тест flaky, что причина найдена или что UI готов.</code></pre>\n<p>PASS проверяет восемнадцать утверждений: вход помечен как memory-only, браузер не запускается, первая и повторная попытки различаются ровно теми полями, которые заданы в модели, а увеличение timeout отклоняется учебным планом. Отдельная отрицательная ветка делает selector множественным и получает другую классификацию. Это важно: даже одинаковый финальный статус «flaky» не даёт права без проверки переписать locator, readiness condition и retry policy одной правкой.</p>\n<figure><img src=\"/assets/editorial/2023/e2e-stability-2023-flake-classification.svg\" alt=\"Схема классификации e2e-сбоя: сначала проверяется уникальность selector, затем отдельное semantic readiness condition, после этого сравниваются initial attempt и retry; trace-shaped evidence остаётся отдельным входом к проверке, а не причиной. Два выхода предлагают уточнить locator либо контракт готовности, а повышение timeout вынесено в отложенное решение.\" loading=\"lazy\" /><figcaption>Схема задаёт порядок разбора. Она не показывает настоящий trace, браузер, длительности, количество флаков или совместимость платформ. Каждый прямоугольник — вопрос к одному слою теста.</figcaption></figure>\n<h2>Маршрут: симптом → причина → проверка → действие</h2>\n<ol><li><strong>Симптом.</strong> Первый запуск failed, retry passed, а отчёт пометил тест flaky. Сохраните номер попытки, исходный текст ошибки и ссылку на evidence, если ваш runner его записал. Не называйте это доказанной причиной.</li><li><strong>Причина.</strong> Обычно в одном ожидании оказались selector, техническая готовность control и результат операции. Иногда к ним добавляют общий timeout, поэтому ошибка становится длиннее, но не яснее.</li><li><strong>Проверка selector.</strong> Убедитесь, что locator на обеих попытках разрешается ровно в один пользовательский элемент. Если нет, это отдельная задача: сузить role/name, scope или test id; не менять пока бизнес-assert.</li><li><strong>Проверка readiness.</strong> Выпишите факт, который обязан увидеть пользователь после действия. Сверьте, что assertion ждёт именно его, а не disabled button, исчезновение spinner или окончание произвольной паузы.</li><li><strong>Проверка retry и evidence.</strong> Сопоставьте initial и retry как два запуска. Для Playwright 1.37.0 retry выполняется в новом worker; trace с <code>on-first-retry</code>, если он настроен, относится к первой повторной попытке. Он помогает задать вопрос, но не возвращает автоматически evidence первого отказа.</li><li><strong>Действие.</strong> Внесите минимальную правку в один слой: locator, semantic assertion, изоляцию данных или явный проектный контракт. Не повышайте timeout, пока не можете назвать, что именно должно стать ready.</li><li><strong>Rollback.</strong> До merge запишите прежний assertion и критерий возврата. Если новый сигнал оказался неверным, откатите только test diff и повторите разбор; не стирайте историю первой неудачной попытки комментарием «пофиксили flaky».</li></ol>\n<h2>Retry полезен как граница сбора evidence, а не как индульгенция</h2>\n<p>В Playwright retries выключены по умолчанию. При включении runner повторно запускает упавший тест; документация v1.37.0 называет flaky тот случай, когда первая попытка не прошла, а повторная прошла. Это удобный сигнал для очереди разбора, но не диагноз. Повтор может дать другой worker и чистое состояние, а значит скрыть утечку тестовых данных, зависимость от порядка или неявную готовность экрана. Он не доказывает, что первая ошибка была случайной, внешний сервис был медленным или selector корректен.</p>\n<p>Конфигурация <code>trace: on-first-retry</code> привязывает артефакт к первому повтору, а не ко всей истории. Это evidence retry, не объяснение initial failure. Если нужен контекст первого отказа, проекту требуется отдельный способ его сохранить с учётом чувствительности данных и цены артефактов.</p>\n<pre><code>// Иллюстрация для Playwright v1.37.0; этот фрагмент не запускается fixture.\nimport { defineConfig } from '@playwright/test';\n\nexport default defineConfig({\n retries: process.env.CI ? 1 : 0,\n use: { trace: 'on-first-retry' },\n});\n\n// retry сохраняет evidence первого повторного запуска.\n// Он не заменяет явное условие готовности после click.</code></pre>\n<h2>Ограничения и следующий проверяемый шаг</h2>\n<p>Заметка не измеряет flake rate, не сравнивает браузеры, не обещает устойчивость любого <code>getByRole()</code> и не предлагает общий timeout. Trace фиксирует один запуск, а причина может лежать в приложении, сети, окружении или данных. Рамка ограничена Playwright v1.37.0 от 10 августа 2023; другую версию проверяют по её документации.</p>\n<p>Следующий шаг — взять один тест со статусом flaky и заполнить короткую карточку из пяти строк: selector, readiness condition, initial outcome, retry outcome и доступный evidence. Затем выбрать один слой для изменения и указать rollback. Если карточку нельзя заполнить без догадок, не увеличивайте timeout. Сначала добавьте недостающий наблюдаемый результат или изоляцию данных. Так retry перестаёт превращать ошибку в шум и становится точкой, где начинается инженерный разбор.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://github.com/microsoft/playwright/releases/tag/v1.37.0\" target=\"_blank\" rel=\"noopener noreferrer\">Playwright v1.37.0: официальный GitHub release, 10 августа 2023</a> — версионный срез, опубликованный до конца августа 2023. Он фиксирует выпуск, но не подтверждает версию, браузеры или настройку конкретного проекта.</li><li><a href=\"https://raw.githubusercontent.com/microsoft/playwright/v1.37.0/docs/src/actionability.md\" target=\"_blank\" rel=\"noopener noreferrer\">Playwright v1.37.0: Auto-waiting, исходник официальной документации по тегу</a> — для click описаны проверки attached, visible, stable, receives events и enabled. Эти проверки готовят действие с элементом, но не доказывают бизнес-результат после действия.</li><li><a href=\"https://raw.githubusercontent.com/microsoft/playwright/v1.37.0/docs/src/test-retries-js.md\" target=\"_blank\" rel=\"noopener noreferrer\">Playwright v1.37.0: Retries, исходник официальной документации по тегу</a> — описывает повторный запуск после сбоя, новый worker и статусы passed, flaky, failed. Статус flaky — классификация результата запусков, а не причина сбоя.</li><li><a href=\"https://raw.githubusercontent.com/microsoft/playwright/v1.37.0/docs/src/trace-viewer.md\" target=\"_blank\" rel=\"noopener noreferrer\">Playwright v1.37.0: Trace Viewer, исходник официальной документации по тегу</a> — описывает действия, snapshots, action log, source и network log; режим on-first-retry записывает trace при первом retry. Такой артефакт — evidence одного запуска, а не доказательство корневой причины.</li><li><a href=\"https://raw.githubusercontent.com/microsoft/playwright/v1.37.0/docs/src/best-practices-js.md\" target=\"_blank\" rel=\"noopener noreferrer\">Playwright v1.37.0: Best Practices, исходник официальной документации по тегу</a> — рекомендует изоляцию тестов, пользовательские locator и web-first assertions. Рекомендация не заменяет проверку реального контракта конкретного экрана.</li></ul>"
|
||
}
|