Тестирование ПО E2E и UI-тесты: Playwright, Selenium, борьба с хрупкостью
0%

E2E и UI-тесты: Playwright, Selenium, борьба с хрупкостью

E2E и UI-тесты: Playwright, Selenium, борьба с хрупкостью

E2E-тесты — единственный уровень, который проверяет то, за что платит пользователь: что система целиком, со всеми сервисами, базой, очередями и фронтендом, выполняет осмысленный сценарий. И одновременно это самый дорогой, самый медленный и самый ненадёжный уровень из всех.

Эта двойственность определяет всё содержание статьи. Мы не будем спорить, нужны E2E или нет — нужны. Вопрос всегда в другом: сколько именно и каких, и как сделать так, чтобы красный тест означал «сломался продукт», а не «сегодня опять CI шалит».

О том, где E2E стоят в общей картине, мы говорили в обзоре курса; про уровни ниже — в модульных и интеграционных тестах.

Что такое E2E и чем он отличается от UI-теста

Термины путают постоянно, а разница практическая.

  • UI-тест — тест, который взаимодействует с системой через графический интерфейс. Это про способ доступа. UI-тест может быть при этом полностью замоканным: фронтенд поднят локально, все бэкенд-запросы перехвачены и отвечают фикстурами.
  • E2E-тест (сквозной) — тест, который проходит сценарий через все реальные слои системы. Это про глубину. E2E может быть вообще без UI: положили сообщение в Kafka, дождались строки в витрине, проверили ответ отчётного API.

Пересечение этих двух множеств — «E2E через UI» — то, что обычно называют «автотестами» в вакансиях, и то, что доставляет 90% боли.

Практический вывод, который экономит команде месяцы: прежде чем писать 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, и между двумя командами состояние страницы может измениться как угодно.

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');
  });
});

Три вещи в этом коде важнее остальных:

  1. expect(...).toBeVisible() — это не проверка, а ожидание с проверкой. Playwright опрашивает условие до таймаута. Поэтому await page.waitForTimeout(3000) в коде теста — почти всегда дефект, а не «стабилизация».
  2. Логин через storageState. Если каждый из 30 тестов проходит форму логина, вы 30 раз тестируете логин и 30 раз рискуете флакнуть на нём. Логин тестируется ровно одним тестом, остальные получают готовую сессию.
  3. Подготовка данных через 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:

  1. Никогда не смешивайте implicit и explicit wait. Неявное ожидание (driver.implicitly_wait(10)) задаёт глобальный таймаут поиска элемента; вместе с WebDriverWait таймауты умножаются непредсказуемо, и NoSuchElement вместо 2 секунд ждёт 40. Документация Selenium прямо это запрещает.
  2. StaleElementReferenceException — не баг фреймворка, а свойство модели. Ссылка на элемент протухает при перерисовке. Лечится тем, что элемент ищется заново непосредственно перед действием, либо ретраем внутри вспомогательного метода.
  3. Свои 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 против авто-ожидания

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, никогда локально — иначе автор не увидит, что написал флаки;
  • не больше одного–двух, иначе тест «зелёный» ценой десяти попыток;
  • факт ретрая обязательно логируется в метрику; тест, прошедший со второй попытки, считается упавшим для статистики здоровья набора;
  • тест, стабильно требующий ретрая, отправляется в карантин, а не живёт вечно.

Карантин без срока годности превращается в кладбище, где сотня тестов «выполняется», но их результат никто не смотрит. Дедлайн и владелец — обязательная часть механизма.

Диагностика: почему упало

Главная скрытая стоимость 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% набора

Ключевая метрика — третья. Если из десяти красных билдов девять оказались проблемами теста, набор перестаёт быть источником сигнала и становится налогом. В этот момент честнее удалить половину тестов, чем «постепенно чинить».

Типичные ошибки

  1. Пирамида вверх ногами. 500 UI-тестов и 50 юнитов. Симптом: любое падение разбирается часами, потому что тест проходит через восемь слоёв и не сообщает, какой из них сломан.
  2. Логин через UI в каждом тесте. Умножает время и флаки на количество тестов.
  3. Зависимость тестов от порядка. «Тест B работает только после теста A» — набор невозможно распараллелить и невозможно перезапустить частично.
  4. Ассерт на текст, приходящий с бэкенда, вместе с проверкой самого бэкенда. Тест падает, но непонятно, кто виноват: вёрстка или API.
  5. Условная логика в тесте. if (await banner.isVisible()) await banner.close(); означает, что вы не знаете состояние системы. Иногда неизбежно, но каждый такой if — скрытая ветка, которая никогда не проверяется.
  6. Ретрай как политика. retries: 5 превращает набор в генератор случайных зелёных галочек.
  7. Сон вместо ожидания. Обсуждали выше; остаётся самой частой правкой при ревью.
  8. E2E как замена ручному тестированию. Автотест проверяет только то, что в него заложили. «Всё зелёное» и «продукт работает» — разные утверждения; см. ручное тестирование.

Мини-итог

  • E2E проверяет то, что нельзя проверить дешевле — и только это. Всё остальное опускается на уровень API, интеграции или юнитов.
  • Стоимость E2E — это стоимость владения, а не написания: поддержка, разбор падений, инфраструктура. Двести UI-тестов — это ставка инженера.
  • Playwright по архитектуре устраняет класс флаки, который в Selenium приходится лечить руками. Но ни один инструмент не спасёт от общих тестовых данных и зависимости тестов от порядка.
  • Флаки — это обнаруженная недетерминированность, иногда в тесте, иногда в продукте. Ретрай маскирует симптом; карантин с владельцем и дедлайном лечит.
  • Набор без метрик здоровья деградирует. Главный показатель — доля красных билдов, за которыми стоял настоящий баг.

Источники

Что дальше

Мы разобрали самый дорогой уровень автоматизации и научились ограничивать его аппетиты. Следующий шаг — уровень, который в большинстве проектов даёт лучшее соотношение сигнала к стоимости и снимает нагрузку с E2E:

Тестирование API: REST, GraphQL, gRPC, схемы и инструменты

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов