Браузерный тест запускается в изолированном контексте, но база данных — внешняя и общая. Два теста, которые работают с одной записью, мешают друг другу. Вход через форму в каждом тесте крадёт минуты на большом наборе. Эта статья о том, как правильно готовить данные и управлять аутентификацией в e2e-тестах.
Почему не стоит готовить данные через UI
Самый очевидный способ — создавать нужное состояние кликами: чтобы проверить корзину, сначала кликами создать товар, зарегистрировать продавца и так далее. Это работает, но плохо.
Проблема первая — скорость. Каждый «подготовительный» клик добавляет секунды. Сотня тестов с таким подходом превращается в многоминутный прогон.
Проблема вторая — хрупкость. Тест корзины падает из-за бага в форме создания товара, который к корзине никакого отношения не имеет. Найти причину трудно.
Правильный подход — готовить данные кратчайшим путём, обычно через API:
test("в корзине виден добавленный товар", async ({ page, request }) => {
const res = await request.post("/api/products", {
data: { name: "Кофемолка", price: 4990 },
});
const product = await res.json();
await page.goto(`/products/${product.id}`);
await page.getByRole("button", { name: "В корзину" }).click();
await expect(page.getByText("Кофемолка")).toBeVisible();
});
request — встроенная фикстура Playwright, HTTP-клиент. Через UI проходит только то, что тест реально проверяет. Остальное — через API. Быстрее и устойчивее.
Аутентификация: один раз вместо каждый раз
Если каждый тест начинается с заполнения формы входа — это лишняя работа. При ста тестах сто форм входа.
Playwright решает это через storageState. Идея простая: один раз пройти вход, сохранить cookies и localStorage в файл, а потом отдавать этот файл каждому тесту — браузер стартует уже залогиненным.
Шаг 1. Сохранить состояние сессии
Обычно это делают в globalSetup — файле, который запускается один раз перед всем набором:
// global-setup.ts
import { chromium } from "@playwright/test";
export default async function globalSetup() {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto("/login");
await page.getByLabel("Email").fill("user@example.com");
await page.getByLabel("Пароль").fill("secret");
await page.getByRole("button", { name: "Войти" }).click();
await page.context().storageState({ path: "auth/user.json" });
await browser.close();
}
Шаг 2. Подключить в конфигурации
// playwright.config.ts
export default defineConfig({
globalSetup: "./global-setup.ts",
use: {
storageState: "auth/user.json",
},
});
Теперь каждый тест стартует с уже готовой сессией. Форма входа — один раз, не сто.
Несколько ролей — несколько файлов
Если приложение различает роли (администратор, обычный пользователь, менеджер), готовят отдельный файл состояния для каждой роли и разные projects в конфигурации:
// playwright.config.ts
projects: [
{ name: "admin", use: { storageState: "auth/admin.json" } },
{ name: "user", use: { storageState: "auth/user.json" } },
],
Тесты для администратора запускаются в admin-проекте, для обычного пользователя — в user.
Подготовка и очистка на нужном уровне
Подготовку данных и их удаление выносят на тот уровень, которому они принадлежат:
globalSetup/globalTeardown— то, что нужно один раз для всего набора: создать auth-файлы, поднять тестовую базу, засеять справочные данные.- Фикстуры теста — данные конкретного теста, созданные в
beforeEachили через собственные фикстуры. Удаляются вafterEach.
Очистка не менее важна, чем подготовка. Тест должен убирать за собой созданное — иначе копится мусор и тесты начинают влиять друг на друга.
test("редактирование статьи", async ({ page, request }) => {
const res = await request.post("/api/articles", { data: { title: "Черновик" } });
const article = await res.json();
// ... тест ...
await request.delete(`/api/articles/${article.id}`); // убираем за собой
});
Изоляция: тест не зависит от чужих данных
Главное правило: тест не должен рассчитывать на то, что кто-то другой уже создал нужные данные.
Каждый тест создаёт своё. Имена и идентификаторы делают уникальными, чтобы параллельные прогоны не сталкивались:
const uniqueName = `Товар-${Date.now()}`;
await request.post("/api/products", { data: { name: uniqueName } });
Зависимость от общего изменяемого состояния — частая причина нестабильных тестов, особенно при параллельном запуске в CI.
Если тесту нужно что-то «всегда существующее» (справочник городов, список категорий), это засевают один раз в globalSetup и не трогают. Всё остальное создаётся и удаляется в рамках конкретного теста.
Коротко
- Готовить данные через API, не через UI: быстрее и устойчивее — тест падает только по своей причине.
storageStateпозволяет залогиниться один раз и переиспользовать сессию во всех тестах.- Для разных ролей — разные файлы состояния и разные
projectsв конфигурации. globalSetup— для того, что нужно один раз; фикстуры — для данных конкретного теста.- Каждый тест убирает за собой созданное, чтобы не накапливался мусор.
- Уникальные имена и идентификаторы помогают избежать конфликтов при параллельном запуске.
- Тест не полагается на данные, оставленные другим тестом.
Что почитать дальше
- Структура тестов и фикстуры — как организовать переиспользуемую подготовку через фикстуры.
- Управление сетью в Playwright — перехват запросов и управление ответами.
- CI и параллельный запуск — как изоляция данных помогает при параллельном прогоне.