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 — после теста остаются данные в БД и портят следующий запуск
Связанные темы
- _MOC Тестирование
- E2E тестирование
- Page Object Model
- Flaky Tests -- причины и решения
- Playwright -- обзор
- Cypress -- установка и основы