Files

8 lines
20 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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 &#039;@playwright/test&#039;;\n\ntest(&#039;подтверждает оплату&#039;, async ({ page }) =&gt; {\n await page.getByRole(&#039;button&#039;, { name: &#039;Оплатить&#039; }).click();\n await expect(page.getByTestId(&#039;payment-state&#039;)).toHaveText(&#039;confirmed&#039;);\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 &#039;@playwright/test&#039;;\n\nexport default defineConfig({\n retries: process.env.CI ? 1 : 0,\n use: { trace: &#039;on-first-retry&#039; },\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>"
}