E2E: паттерны и антипаттерны

Паттерны E2E тестирования — проверенные подходы к написанию надёжных, поддерживаемых end-to-end тестов; антипаттерны — типичные ошибки, превращающие E2E suite в источник flakiness и технического долга.

Зачем нужно

E2E тесты дают максимальную уверенность в работе системы, но они же самые дорогие: медленные, хрупкие, требуют реального окружения. Знание паттернов позволяет получить от E2E максимум пользы при минимальных затратах на поддержку.

Где используется

  • Cypress и Playwright проекты любого масштаба
  • CI/CD pipeline с E2E stage
  • Команды, страдающие от flaky tests

Основной контент

Паттерн: Page Object Model (POM)

// pages/LoginPage.ts
export class LoginPage {
  constructor(private page: Page) {}

  async goto {
    await this.page.goto('/login');
  }

  async login(email: string, password: string) {
    await this.page.fill('[data-testid="email"]', email);
    await this.page.fill('[data-testid="password"]', password);
    await this.page.click('[data-testid="submit"]');
  }

  async getErrorMessage {
    return this.page.textContent('[data-testid="error"]');
  }
}

// test
test('вход с валидными данными', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto;
  await loginPage.login('user@example.com', 'secret');
  await expect(page).toHaveURL('/dashboard');
});

Паттерн: data-testid атрибуты

<!-- В компоненте — стабильный селектор, не зависящий от стилей -->
<button data-testid="add-to-cart" class="btn btn-primary">Добавить</button>
await page.click('[data-testid="add-to-cart"]');
// Не: await page.click('.btn-primary') — ломается при смене стилей

Паттерн: изоляция состояния через API

test.beforeEach(async ({ request }) => {
  // Сбрасываем данные через API, не через UI
  await request.post('/api/test/reset-db');
  await request.post('/api/users', { data: { email: 'test@example.com', password: 'pass' } });
});

Паттерн: мокирование внешних сервисов

// Playwright — перехват запросов к внешнему API
await page.route('**/api/payments/**', (route) => {
  route.fulfill({ status: 200, body: JSON.stringify({ status: 'success' }) });
});

Антипаттерн: жёсткие задержки

// ПЛОХО
await page.waitForTimeout(3000);
await page.click('#submit');

// ХОРОШО — ждём конкретного состояния
await page.waitForSelector('#submit:not([disabled])');
await page.click('#submit');

Антипаттерн: зависимость тестов друг от друга

// ПЛОХО — тест 2 зависит от результата теста 1
test('тест 1: создаёт пользователя', async () => { /* ... */ });
test('тест 2: редактирует пользователя', async () => { /* зависит от теста 1 */ });

// ХОРОШО — каждый тест независим
test.beforeEach(async ({ request }) => {
  await request.post('/api/users', { data: testUser });
});

Антипаттерн: тестирование через UI то, что можно через unit

// ПЛОХО — E2E тест для проверки валидации email
test('валидирует формат email', async ({ page }) => {
  await page.fill('#email', 'invalid');
  await page.click('#submit');
  await expect(page.locator('.error')).toBeVisible;
});

// ЛУЧШЕ — логику валидации покрыть unit-тестом,
// в E2E только критичный happy path

Частые ошибки

  • Слишком много E2E тестов — E2E должны покрывать только критичные пользовательские пути; детали — задача unit и integration тестов
  • Нет повторного запуска flaky тестов — в CI включай retry для E2E (--retries 2 в Playwright)
  • Тесты без cleanup — после теста остаются данные в БД и портят следующий запуск

Связанные темы

Ресурсы