E2E и UI-тесты: Playwright, Selenium, борьба с хрупкостью
E2E-тесты — единственный уровень, который проверяет то, за что платит пользователь: что система целиком, со всеми сервисами, базой, очередями и фронтендом, выполняет осмысленный сценарий. И одновременно это самый дорогой, самый медленный и самый ненадёжный уровень из всех.
Эта двойственность определяет всё содержание статьи. Мы не будем спорить, нужны E2E или нет — нужны. Вопрос всегда в другом: сколько именно и каких, и как сделать так, чтобы красный тест означал «сломался продукт», а не «сегодня опять CI шалит».
О том, где E2E стоят в общей картине, мы говорили в обзоре курса; про уровни ниже — в модульных и интеграционных тестах.
Что такое E2E и чем он отличается от UI-теста
Термины путают постоянно, а разница практическая.
- UI-тест — тест, который взаимодействует с системой через графический интерфейс. Это про способ доступа. UI-тест может быть при этом полностью замоканным: фронтенд поднят локально, все бэкенд-запросы перехвачены и отвечают фикстурами.
- E2E-тест (сквозной) — тест, который проходит сценарий через все реальные слои системы. Это про глубину. E2E может быть вообще без UI: положили сообщение в Kafka, дождались строки в витрине, проверили ответ отчётного API.
Пересечение этих двух множеств — «E2E через UI» — то, что обычно называют «автотестами» в вакансиях, и то, что доставляет 90% боли.
фронтенд + перехваченная сеть
секунды, стабильно"] B["E2E через UI
браузер + все сервисы + БД
минуты, хрупко"] C["E2E без UI
API/очереди + все сервисы
десятки секунд, средне"] end A -->|"добавили реальный бэкенд"| B C -->|"добавили браузер"| B style A fill:#4caf82,fill-opacity:0.18 style B fill:#d1655a,fill-opacity:0.18 style C fill:#e0a252,fill-opacity:0.18
Практический вывод, который экономит команде месяцы: прежде чем писать E2E через UI, спросите, нельзя ли получить тот же сигнал дешевле. Проверка «скидка 20% считается корректно» не требует браузера. Проверка «кнопка «Оплатить» доступна с клавиатуры и ведёт на форму эквайринга» — требует.
Взгляд тестировщика: E2E — это то, что ближе всего к реальному пользователю, и единственный способ поймать проблемы интеграции конфигураций, которые не видит ни один юнит-тест. Взгляд разработчика: E2E — это то, что падает в чужом PR по непонятной причине и блокирует релиз на два часа.
Оба описывают одну и ту же систему с разных сторон. Хороший E2E-набор — маленький, быстрый и с нулевой терпимостью к флаки; тогда обе стороны довольны.
Как решать, что покрывать E2E
Стоимость E2E-теста складывается не из написания, а из владения. Реалистичная арифметика для типового веб-продукта:
| Статья затрат | Оценка на один E2E-сценарий в год |
|---|---|
| Написание и отладка | 4–12 часов |
| Поддержка при изменениях UI | 2–6 часов |
| Разбор ложных падений (при flake rate 2%, 200 прогонов) | 4 часа |
| Доля инфраструктуры (агенты CI, стенды) | десятки долларов |
Двести E2E-тестов — это примерно одна полноценная ставка инженера, полностью занятого их обслуживанием. Это нормально, если они защищают выручку, и катастрофа, если они дублируют юнит-тесты через браузер.
Отсюда рабочий критерий отбора: E2E покрывают деньги и репутацию. Сценарий попадает в набор, если его поломка означает потерю денег, утечку данных или публичный инцидент, и если ни один более дешёвый уровень не даёт того же сигнала.
Правый нижний квадрант — главный источник мусора в реальных проектах. «Экспорт в PDF» ломается от каждого изменения шрифта, разбирается полдня, а сломанный экспорт стоит компании одного тикета в поддержку. Такие вещи уходят в ручной регресс-чек-лист (см. ручное тестирование) или в дешёвую проверку на уровне API: «эндпоинт вернул 200 и Content-Type: application/pdf».
Практический ориентир по объёму для продукта средней сложности: 10–40 E2E-сценариев через UI, полный прогон — не дольше 10–15 минут в параллели. Если у вас 800 UI-тестов и прогон полтора часа, проблема не в инфраструктуре, а в стратегии; подробно — в статье про стратегию автоматизации.
Как устроены инструменты: Selenium против Playwright
Разница в архитектуре — не академическая, она напрямую объясняет разницу в стабильности.
Selenium (WebDriver Classic, W3C) — это протокол поверх HTTP. Ваш код шлёт команды на сервер-драйвер, драйвер транслирует их браузеру. Каждое действие — отдельный сетевой round-trip, и между двумя командами состояние страницы может измениться как угодно.
список — elementId уже мёртв T->>D: POST /element/{id}/click D->>B: клик B-->>D: StaleElementReferenceException D-->>T: ошибка
Playwright работает иначе: он общается с браузером по управляющему протоколу
(CDP для Chromium, собственные каналы для Firefox и WebKit), через один
двунаправленный канал, и — ключевое — локатор в нём ленивый. page.getByRole(...)
не ищет элемент немедленно, он описывает как искать. Поиск происходит в момент
действия, и повторяется, пока элемент не станет «actionable»: присутствует в DOM,
видим, стабилен по координатам, включён, не перекрыт другим элементом.
Именно эта «ленивость + перепроверка» убирает целый класс флаки, с которым в Selenium приходится бороться руками через явные ожидания.
Сравнение по существу
| Selenium | Playwright | Cypress | |
|---|---|---|---|
| Протокол | WebDriver Classic / BiDi | CDP и родные протоколы | код исполняется в браузере |
| Авто-ожидание | нет, только явные WebDriverWait |
встроено в каждое действие | встроено |
| Языки | Java, Python, C#, JS, Ruby, Kotlin | TS/JS, Python, Java, .NET | только JS/TS |
| Несколько вкладок/доменов | да | да | ограниченно |
| Перехват сети | через прокси/BiDi | из коробки (page.route) |
из коробки |
| Параллелизм | через Grid | из коробки, процессы-воркеры | платно/через CI |
| Экосистема и легаси | огромная, стандарт W3C | молодая, быстро растёт | фронтенд-ориентированная |
Практическая рекомендация без религии: новый проект на вебе — Playwright. Selenium остаётся правильным выбором, когда нужна поддержка старых браузеров или корпоративных сборок, когда уже есть большой набор на Java/C#, когда требуется работа с нативными окнами через Grid, или когда стандарт W3C — формальное требование. Спецификация WebDriver BiDi постепенно сближает подходы: Selenium получает двунаправленный канал и перехват сети.
Playwright на практике
Минимальный, но продакшн-пригодный тест. Обратите внимание: ни одного sleep,
ни одного CSS-селектора по классам, вход выполняется через API, а не через UI.
// tests/checkout.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Оформление заказа', () => {
// Состояние логина восстанавливается из файла — UI логина не участвует
test.use({ storageState: 'playwright/.auth/customer.json' });
test('оплата картой создаёт заказ в статусе «Оплачен»', async ({ page, request }) => {
// 1. Данные готовим через API: быстро, детерминированно, без кликов
const cart = await request.post('/api/test/carts', {
data: { items: [{ sku: 'SKU-42', qty: 2 }] },
});
expect(cart.ok()).toBeTruthy();
const { id: cartId } = await cart.json();
// 2. Через UI проходим только то, ради чего этот тест существует
await page.goto(`/checkout/${cartId}`);
await page.getByRole('textbox', { name: 'Номер карты' }).fill('4111111111111111');
await page.getByRole('textbox', { name: 'CVC' }).fill('123');
await page.getByRole('button', { name: 'Оплатить' }).click();
// 3. Ждём НЕ время, а наблюдаемое состояние. Веб-first assertion сам ретраится
await expect(page.getByRole('heading', { name: 'Заказ оформлен' })).toBeVisible();
await expect(page.getByTestId('order-status')).toHaveText('Оплачен');
// 4. Проверяем, что изменился и бэкенд, а не только картинка
const order = await request.get(`/api/orders/by-cart/${cartId}`);
expect((await order.json()).status).toBe('PAID');
});
});
Три вещи в этом коде важнее остальных:
expect(...).toBeVisible()— это не проверка, а ожидание с проверкой. Playwright опрашивает условие до таймаута. Поэтомуawait page.waitForTimeout(3000)в коде теста — почти всегда дефект, а не «стабилизация».- Логин через
storageState. Если каждый из 30 тестов проходит форму логина, вы 30 раз тестируете логин и 30 раз рискуете флакнуть на нём. Логин тестируется ровно одним тестом, остальные получают готовую сессию. - Подготовка данных через API. Создавать заказ кликами в UI, чтобы проверить его отмену, — это два теста в одном, где первый ломает второй.
Page Object и фикстуры
Page Object — шаблон Мартина Фаулера: обёртка, скрывающая структуру страницы за осмысленными операциями. Его главная ценность — единая точка правки, когда вёрстка поменялась.
// pages/CheckoutPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class CheckoutPage {
private readonly cardNumber: Locator;
private readonly payButton: Locator;
readonly orderStatus: Locator;
constructor(private readonly page: Page) {
// Локаторы ленивые: их можно создать в конструкторе до открытия страницы
this.cardNumber = page.getByRole('textbox', { name: 'Номер карты' });
this.payButton = page.getByRole('button', { name: 'Оплатить' });
this.orderStatus = page.getByTestId('order-status');
}
async open(cartId: string) {
await this.page.goto(`/checkout/${cartId}`);
await expect(this.payButton).toBeEnabled(); // страница действительно готова
}
async payWithCard(number: string, cvc: string) {
await this.cardNumber.fill(number);
await this.page.getByRole('textbox', { name: 'CVC' }).fill(cvc);
await this.payButton.click();
}
}
Две типичные ошибки при работе с Page Object:
- Ассерты внутри Page Object. Метод
checkOrderIsPaid()прячет проверку от читателя теста; тест становится нечитаемым списком вызовов. Page Object отдаёт локаторы и действия, проверки живут в тесте. - Page Object как «god object» на 900 строк. Модель должна следовать компонентам
интерфейса, а не страницам:
CartWidget,PaymentForm,HeaderNav.
Альтернатива на больших наборах — Screenplay pattern (акторы и задачи вместо страниц), удобный, когда один сценарий проходят несколько ролей пользователей. В Playwright ту же задачу часто решают проще — собственными фикстурами:
// fixtures.ts — «свой» test с готовыми объектами
import { test as base } from '@playwright/test';
import { CheckoutPage } from './pages/CheckoutPage';
export const test = base.extend<{ checkout: CheckoutPage }>({
checkout: async ({ page }, use) => {
await use(new CheckoutPage(page));
// здесь же — гарантированная уборка после теста
},
});
export { expect } from '@playwright/test';
Selenium на практике
Тот же принцип на Python. Ключевое отличие — ожидания приходится писать явно, и это ровно то место, где рождаются флаки.
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
def test_checkout_pays_with_card(driver: webdriver.Remote, cart_id: str) -> None:
wait = WebDriverWait(driver, timeout=10, poll_frequency=0.2)
driver.get(f"https://shop.example/checkout/{cart_id}")
# Плохо: driver.find_element(...).send_keys(...) — элемента может ещё не быть
# Хорошо: ждём именно нужного состояния, а не «немного времени»
card = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid=card-number]")))
card.send_keys("4111111111111111")
driver.find_element(By.CSS_SELECTOR, "[data-testid=cvc]").send_keys("123")
wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid=pay]"))).click()
# Ждём наблюдаемый результат, а не «прошло 5 секунд»
status = wait.until(
EC.text_to_be_present_in_element((By.CSS_SELECTOR, "[data-testid=order-status]"), "Оплачен")
)
assert status
Три правила выживания в Selenium:
- Никогда не смешивайте implicit и explicit wait. Неявное ожидание
(
driver.implicitly_wait(10)) задаёт глобальный таймаут поиска элемента; вместе сWebDriverWaitтаймауты умножаются непредсказуемо, иNoSuchElementвместо 2 секунд ждёт 40. Документация Selenium прямо это запрещает. StaleElementReferenceException— не баг фреймворка, а свойство модели. Ссылка на элемент протухает при перерисовке. Лечится тем, что элемент ищется заново непосредственно перед действием, либо ретраем внутри вспомогательного метода.- Свои
expected_conditions. Стандартных условий мало; напишите условие вида «спиннер исчез и таблица содержит N строк» — оно устраняет десяткиsleep.
Для управления браузерами в CI используйте docker-selenium или Selenium Grid: воспроизводимые версии браузеров важнее, чем кажется — половина «внезапных» падений это автообновившийся Chrome на агенте.
Хрупкость: откуда она берётся
Флаки-тест — тест, который на неизменном коде даёт разный результат. Google в своих исследованиях сообщал, что около 1,5% всех тестовых прогонов флакают, и это одна из главных проблем их инфраструктуры (Flaky Tests at Google, Test Flakiness). На UI-уровне доля выше на порядок.
Важно понять природу проблемы: флаки-тест — это не «плохой тест», а тест, который обнаружил недетерминированность. Иногда недетерминирован тест. Но иногда — сам продукт: гонка на фронтенде, не идемпотентный обработчик, неверная блокировка. Прежде чем «стабилизировать» тест ретраем, потратьте полчаса на вопрос «а точно ли ошибается тест?». Классика темы — «Eradicating Non-Determinism in Tests» Мартина Фаулера.
Практики борьбы
1. Локаторы: подниматься по шкале устойчивости
Рекомендуемый порядок выбора совпадает у Playwright и Testing Library
(priority of queries):
сначала роль + доступное имя, затем label/placeholder, затем текст, и только потом
data-testid — и никогда структурный XPath.
Роль лучше data-testid по неочевидной причине: getByRole('button', { name: 'Оплатить' })
падает, если кнопка перестала быть доступной для скринридера. Это бесплатная
проверка доступности внутри функционального теста (подробнее — в статье про
кроссбраузерность и доступность).
data-testid же стабилен всегда — в том числе когда интерфейс окончательно сломан
для реальных людей.
Практический компромисс: роль + имя как основной инструмент, data-testid —
для элементов без семантики (контейнеры, кастомные виджеты, строки таблицы).
И договоритесь с командой, что data-testid — часть контракта, а не «мусор в разметке,
который можно удалить при рефакторинге».
2. Ожидания: только условия, никогда время
sleep плох дважды: на быстрой машине он тратит время впустую, на медленной —
всё равно не спасает. Заменяется ожиданием наблюдаемого условия:
// Плохо — «магическое число», подобранное под ноутбук автора
await page.waitForTimeout(3000);
await expect(page.getByTestId('total')).toHaveText('2 400 ₽');
// Хорошо — ждём исчезновения индикатора и конкретного значения
await expect(page.getByTestId('spinner')).toBeHidden();
await expect(page.getByTestId('total')).toHaveText('2 400 ₽');
// Ещё лучше, если UI обновляется после запроса — ждём сам запрос
const resp = page.waitForResponse(r => r.url().includes('/api/cart') && r.status() === 200);
await page.getByRole('button', { name: 'Пересчитать' }).click();
await resp;
Отключение анимаций на уровне конфигурации снимает целый класс проблем
со «стабильностью координат»: reducedMotion: 'reduce' в Playwright плюс CSS
* { animation-duration: 0s !important; transition-duration: 0s !important; }
для тестового окружения.
3. Данные: изоляция вместо общего стенда
Самый частый источник «падает только в параллели» — общий пользователь. Правило: каждый тест создаёт своё состояние и не зависит от порядка выполнения.
# Плохо: общий аккаунт, тесты дерутся за одну корзину
LOGIN = "qa@example.com"
# Хорошо: уникальный пользователь на тест, создаётся через API за ~50 мс
def make_user() -> dict:
suffix = uuid.uuid4().hex[:12]
return api.post("/api/test/users", json={
"email": f"qa+{suffix}@example.com",
"password": "Test!2345",
}).json()
Уборка данных предпочтительнее в начале теста (setup), а не в конце (teardown): если прогон упал, teardown может не выполниться, а следующий запуск всё равно получит чистое состояние. Это же оставляет вам сломанное состояние для разбора.
4. Границы системы: что мокать, а что нет
Догма «E2E должен ходить в реальные системы» ломается о реальность: платёжный шлюз партнёра ложится по субботам, капча не решается принципиально, SMS стоит денег.
Рабочее правило: всё, что внутри вашей зоны ответственности, — реально; всё, что снаружи и не воспроизводимо, — стабильный дубль. Sandbox-режим внешнего сервиса — оптимум, если он есть.
// Отрезаем аналитику и рекламу: они не проверяются, но роняют тесты
await page.route(/(google-analytics|doubleclick|hotjar)\.com/, r => r.abort());
// Внешняя платёжка: детерминированный ответ вместо капризного sandbox
await page.route('**/api/payment/authorize', route =>
route.fulfill({ status: 200, json: { status: 'AUTHORIZED', txId: 'tx-test-1' } }),
);
Отдельно фиксируйте время: тесты, зависящие от «сегодня», ломаются в полночь и 29 февраля. В Playwright для этого есть Clock API, позволяющий подменить системное время страницы.
5. Ретраи: обезболивающее, а не лечение
Ретраи допустимы, но с жёсткими условиями:
- только в CI, никогда локально — иначе автор не увидит, что написал флаки;
- не больше одного–двух, иначе тест «зелёный» ценой десяти попыток;
- факт ретрая обязательно логируется в метрику; тест, прошедший со второй попытки, считается упавшим для статистики здоровья набора;
- тест, стабильно требующий ретрая, отправляется в карантин, а не живёт вечно.
или ценность не доказана Удалён --> [*] note right of Карантин Вынесен из блокирующего гейта, но продолжает выполняться ночью. У теста ОБЯЗАН быть владелец и тикет с дедлайном. end note
Карантин без срока годности превращается в кладбище, где сотня тестов «выполняется», но их результат никто не смотрит. Дедлайн и владелец — обязательная часть механизма.
Диагностика: почему упало
Главная скрытая стоимость E2E — не написание, а разбор падений. Инструменты, которые снижают эту стоимость в разы:
- Trace Viewer в Playwright — запись всего прогона: DOM-снапшот на каждом шаге,
сетевые запросы, консоль, скриншоты до/после действия. Настраивается как
trace: 'on-first-retry'— почти бесплатно по производительности, но даёт полную картину при падении (документация). - Видео и скриншоты только для упавших (
video: 'retain-on-failure'). - Логи бэкенда за окно теста, привязанные по
X-Request-Id, который тест проставляет в заголовки. Без этого разбор «упало на проде-подобном стенде» превращается в гадание. - Осмысленные сообщения ассертов.
expect(locator).toHaveText('Оплачен')уже печатает фактическое значение и HTML — не заменяйте это наexpect(true).toBe(...).
Визуальное тестирование: отдельный зверь
Скриншотные тесты (toHaveScreenshot, Applitools, Percy, BackstopJS) ловят то,
что функциональные тесты не видят: съехавшую вёрстку, пропавший шрифт, чёрный текст
на чёрном фоне.
Честно о цене: это самый флакающий вид тестов. Разный рендеринг шрифтов в macOS и Linux, сглаживание, курсор, анимации, динамические данные — всё даёт диффы. Что делает их применимыми:
- прогон только в контейнере с зафиксированными шрифтами и версией браузера (никаких скриншотов с ноутбука разработчика в базе эталонов);
- маскирование динамических зон:
mask: [page.getByTestId('current-date')]; - порог различия (
maxDiffPixelRatio), а не побайтовое сравнение; - применение на уровне компонентов (Storybook + снапшоты), а не целых страниц — диффы меньше, причина очевиднее.
E2E в CI
Конфигурация Playwright, которую можно брать за основу:
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true, // тесты внутри файла тоже параллельно
forbidOnly: !!process.env.CI, // .only не должен уехать в main
retries: process.env.CI ? 1 : 0, // ретраи только в CI
workers: process.env.CI ? 4 : undefined,
reporter: [['html'], ['github'], ['junit', { outputFile: 'results.xml' }]],
timeout: 60_000, // на весь тест
expect: { timeout: 10_000 }, // на одно ожидание условия
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
trace: 'on-first-retry',
video: 'retain-on-failure',
screenshot: 'only-on-failure',
reducedMotion: 'reduce',
actionTimeout: 15_000,
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile', use: { ...devices['Pixel 7'] } },
],
});
Шардирование — главный рычаг по времени прогона: набор делится на N частей, которые выполняются на разных раннерах.
# .github/workflows/e2e.yml
name: E2E
on: [pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 25
strategy:
fail-fast: false # хотим видеть все шарды, а не первый упавший
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npx playwright install --with-deps chromium
- name: Поднять стенд
run: docker compose -f docker-compose.e2e.yml up -d --wait
- name: Прогон шарда
run: npx playwright test --shard=${{ matrix.shard }}/4
- name: Сохранить отчёт и трейсы
if: always() # артефакты нужны именно когда упало
uses: actions/upload-artifact@v4
with:
name: pw-report-${{ matrix.shard }}
path: |
playwright-report/
test-results/
retention-days: 7
Практика, которая экономит больше всего времени команды: E2E не в каждом PR, а по слоям. На PR — смоук из 5–8 сценариев за 3 минуты; полный набор — на merge в основную ветку и по расписанию ночью. Детали организации гейтов — в статье про тесты в CI.
Метрики здоровья набора
Если у набора нет метрик, он деградирует незаметно, пока команда не начнёт перезапускать сборку рефлекторно. Минимальный набор показателей:
| Метрика | Как считать | Целевое значение |
|---|---|---|
| Flake rate | доля прогонов, где тест упал и прошёл при ретрае | < 0,5%, тревога при > 2% |
| Время полного прогона | p95 по последним 50 прогонам | < 15 минут |
| Доля падений, оказавшихся настоящими багами | разбор тикетов за месяц | > 50% |
| Время разбора падения | от красного билда до вердикта | < 15 минут |
| Тестов в карантине | абсолютное число | < 5% набора |
Ключевая метрика — третья. Если из десяти красных билдов девять оказались проблемами теста, набор перестаёт быть источником сигнала и становится налогом. В этот момент честнее удалить половину тестов, чем «постепенно чинить».
Типичные ошибки
- Пирамида вверх ногами. 500 UI-тестов и 50 юнитов. Симптом: любое падение разбирается часами, потому что тест проходит через восемь слоёв и не сообщает, какой из них сломан.
- Логин через UI в каждом тесте. Умножает время и флаки на количество тестов.
- Зависимость тестов от порядка. «Тест B работает только после теста A» — набор невозможно распараллелить и невозможно перезапустить частично.
- Ассерт на текст, приходящий с бэкенда, вместе с проверкой самого бэкенда. Тест падает, но непонятно, кто виноват: вёрстка или API.
- Условная логика в тесте.
if (await banner.isVisible()) await banner.close();означает, что вы не знаете состояние системы. Иногда неизбежно, но каждый такойif— скрытая ветка, которая никогда не проверяется. - Ретрай как политика.
retries: 5превращает набор в генератор случайных зелёных галочек. - Сон вместо ожидания. Обсуждали выше; остаётся самой частой правкой при ревью.
- E2E как замена ручному тестированию. Автотест проверяет только то, что в него заложили. «Всё зелёное» и «продукт работает» — разные утверждения; см. ручное тестирование.
Мини-итог
- E2E проверяет то, что нельзя проверить дешевле — и только это. Всё остальное опускается на уровень API, интеграции или юнитов.
- Стоимость E2E — это стоимость владения, а не написания: поддержка, разбор падений, инфраструктура. Двести UI-тестов — это ставка инженера.
- Playwright по архитектуре устраняет класс флаки, который в Selenium приходится лечить руками. Но ни один инструмент не спасёт от общих тестовых данных и зависимости тестов от порядка.
- Флаки — это обнаруженная недетерминированность, иногда в тесте, иногда в продукте. Ретрай маскирует симптом; карантин с владельцем и дедлайном лечит.
- Набор без метрик здоровья деградирует. Главный показатель — доля красных билдов, за которыми стоял настоящий баг.
Источники
- Playwright: Best Practices и Locators
- Selenium: Waiting Strategies
- W3C WebDriver BiDi
- Martin Fowler, Eradicating Non-Determinism in Tests, PageObject
- Google Testing Blog: Flaky Tests at Google, Test Flakiness
- Testing Library: Priority of Queries
- docker-selenium
- Jez Humble, David Farley. Continuous Delivery — главы про тестовые окружения и данные
Что дальше
Мы разобрали самый дорогой уровень автоматизации и научились ограничивать его аппетиты. Следующий шаг — уровень, который в большинстве проектов даёт лучшее соотношение сигнала к стоимости и снимает нагрузку с E2E: