Фикстура request в Playwright делает HTTP-запросы без открытия браузера и завершается за 50–200 миллисекунд против 3–5 секунд для UI-теста. Она доступна вместе с page в любом Playwright-тесте, что открывает гибридный паттерн: создавать тестовые данные через API, проверять их через UI, очищать через API. Всё в одном тестовом файле без лишних инструментов. Здесь разобраны GET, POST, аутентификация, тестирование ошибочных ответов и паттерны API+UI которые устраняют флакующую настройку.
Почему API-тесты нужны в твоём Playwright-сьюте
Аргумент за API-тестирование не в замене UI-тестов. Он в тестировании правильных вещей на правильном уровне.
UI-тест создающий travel-запись проходит через браузер, рендерит форму, заполняет поля, кликает submit, ждёт обновления страницы, затем проверяет результат. Это занимает 3–5 секунд и может упасть по дюжине причин не связанных с реальной функцией: медленная анимация, изменённый локатор, флакующий сетевой запрос.
API-тест создающий travel-запись отправляет один HTTP-запрос и проверяет ответ. Это занимает 50–200 миллисекунд и падает только когда само API сломано.
Используй API-тесты для: валидации данных, правил аутентификации, ошибочных ответов, бизнес-логики в бэкенде. Используй UI-тесты для: пользовательских флоу, визуального рендеринга, фронтенд-взаимодействий. Оба имеют место. Использовать только UI-тесты когда баг в API: это ошибка.
Фикстура request
Playwright предоставляет APIRequestContext через фикстуру request. Она доступна в любом тесте так же как page:
import { test, expect } from '@playwright/test';
test('GET /api/items returns a list', async ({ request }) => {
const response = await request.get('https://lab.becomeqa.com/api/items');
expect(response.status()).toBe(200);
const items = await response.json();
expect(items.length).toBeGreaterThan(0);
});Браузер не открывается. Страница не загружается. Просто HTTP-запрос и ответ. Фикстура request автоматически обрабатывает куки, заголовки и конфигурацию baseURL.
GET-запросы и валидация ответа
Наиболее распространённый паттерн API-теста: вызов эндпоинта, проверка статуса, проверка формы данных.
test('GET /api/items returns correct structure', async ({ request }) => {
const response = await request.get('https://lab.becomeqa.com/api/items');
// Проверяем HTTP-статус
expect(response.status()).toBe(200);
expect(response.ok()).toBeTruthy(); // true для 2xx кодов
// Парсим тело ответа
const items = await response.json();
// Проверяем массив
expect(Array.isArray(items)).toBe(true);
expect(items.length).toBeGreaterThan(0);
// Проверяем форму первого элемента
const firstItem = items[0];
expect(firstItem).toHaveProperty('id');
expect(firstItem).toHaveProperty('destination');
expect(firstItem).toHaveProperty('status');
});response.ok() это сокращение для status >= 200 && status < 300. Используй когда важно только что запрос успешен.
Проверка заголовков ответа:
const contentType = response.headers()['content-type'];
expect(contentType).toContain('application/json');POST-запросы
POST-запросы отправляют данные для создания ресурса. Передавай тело как JSON:
test('POST /api/items creates a new item', async ({ request }) => {
const response = await request.post('https://lab.becomeqa.com/api/items', {
data: {
destination: 'Tokyo',
status: 'planned',
notes: 'Cherry blossom season'
}
});
expect(response.status()).toBe(201);
const created = await response.json();
expect(created).toHaveProperty('id');
expect(created.destination).toBe('Tokyo');
expect(created.status).toBe('planned');
});Опция data автоматически сериализует в JSON и устанавливает заголовок Content-Type: application/json.
Для запросов с form-encoded данными используй form вместо data:
const response = await request.post('/api/login', {
form: {
username: 'admin@becomeqa.com',
password: 'testpass123'
}
});Аутентификация
Большинство реальных API требуют аутентификации. Два распространённых паттерна.
Bearer-токен в заголовке
test('authenticated GET returns user data', async ({ request }) => {
// Сначала получаем токен
const loginResponse = await request.post('https://lab.becomeqa.com/api/auth/login', {
data: {
username: 'admin@becomeqa.com',
password: 'testpass123'
}
});
const { token } = await loginResponse.json();
// Используем токен в последующих запросах
const response = await request.get('https://lab.becomeqa.com/api/items', {
headers: {
'Authorization': `Bearer ${token}`
}
});
expect(response.status()).toBe(200);
});Cookie-аутентификация через UI-логин
Если приложение использует куки для auth, можно залогиниться через браузер и переиспользовать сессию для API-вызовов. Объект page.request разделяет куки с контекстом браузера:
test('API call with browser session', async ({ page, request }) => {
// Логинимся через UI
await page.goto('https://lab.becomeqa.com');
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Submit' }).click();
// Теперь используем page.request: он несёт auth-куку
const response = await page.request.get('https://lab.becomeqa.com/api/items');
expect(response.status()).toBe(200);
});Важное различие: request (фикстура) это самостоятельный контекст без куков браузера. page.request разделяет куки с текущей сессией браузера.
Тестирование ошибочных ответов
API должны возвращать значимые ошибки. Тестируй их явно.
test('GET /api/items/:id returns 404 for unknown id', async ({ request }) => {
const response = await request.get('https://lab.becomeqa.com/api/items/nonexistent-id-99999');
expect(response.status()).toBe(404);
const body = await response.json();
expect(body).toHaveProperty('error');
});
test('POST /api/items returns 400 for missing required fields', async ({ request }) => {
const response = await request.post('https://lab.becomeqa.com/api/items', {
data: {
// отсутствует обязательное поле 'destination'
status: 'planned'
}
});
expect(response.status()).toBe(400);
});
test('protected endpoint returns 401 without auth', async ({ request }) => {
const response = await request.get('https://lab.becomeqa.com/api/items');
// нет заголовка Authorization
expect(response.status()).toBe(401);
});Эти тесты проверяют что API корректно обрабатывает некорректный ввод, а не только happy path.
Комбинирование API и UI тестов
Один из наиболее мощных паттернов: используй API для настройки тестовых данных, затем проверяй через UI. Или наоборот: действуй через UI, проверяй через API.
API-настройка, UI-проверка
test('created item appears in the UI table', async ({ page, request }) => {
// Создаём элемент через API: быстро и надёжно
const createResponse = await request.post('https://lab.becomeqa.com/api/items', {
data: { destination: 'Lisbon', status: 'planned' }
});
const { id } = await createResponse.json();
// Проверяем что он появился в UI
await page.goto('https://lab.becomeqa.com');
// ... шаги логина ...
await expect(page.getByText('Lisbon')).toBeVisible();
// Очищаем через API после теста
await request.delete(`https://lab.becomeqa.com/api/items/${id}`);
});Этот тест быстрее и надёжнее. Настройка через UI наиболее частый источник флакинга в тестах которые реально не тестируют UI.
UI-действие, API-проверка
test('deleting an item via UI removes it from the API', async ({ page, request }) => {
// Настройка через API
const createResponse = await request.post('https://lab.becomeqa.com/api/items', {
data: { destination: 'Oslo', status: 'planned' }
});
const { id } = await createResponse.json();
// Действие через UI
await page.goto('https://lab.becomeqa.com');
// ... логин, поиск элемента, клик удаления ...
// Проверка через API
const checkResponse = await request.get(`https://lab.becomeqa.com/api/items/${id}`);
expect(checkResponse.status()).toBe(404);
});Настройка base URL для API-тестов
Повторять полный URL в каждом тесте это шум. Задай baseURL в playwright.config.ts:
export default defineConfig({
use: {
baseURL: 'https://lab.becomeqa.com',
},
});Теперь используй относительные пути:
const response = await request.get('/api/items');
const response = await request.post('/api/items', { data: { ... } });Для проектов с разными базовыми URL для UI и API создай кастомную фикстуру которая настраивает API-контекст отдельно.
extraHTTPHeaders в конфиг если API требует общий заголовок в каждом запросе (например API-ключ или кастомный X-App-Version). Задай один раз в конфиге вместо повторения в каждом тесте.export default defineConfig({
use: {
baseURL: 'https://lab.becomeqa.com',
extraHTTPHeaders: {
'X-Api-Key': process.env.API_KEY || '',
},
},
});Организация API-тестов отдельно
Держи API-тесты в отдельной папке чтобы запускать их независимо от UI-тестов:
tests/
ui/
login.spec.ts
items.spec.ts
api/
items-api.spec.ts
auth-api.spec.ts# Только API-тесты: быстро, без браузера
npx playwright test tests/api/
# Только UI-тесты
npx playwright test tests/ui/API-тесты работают значительно быстрее UI-тестов. Раздельный запуск в CI позволяет получить быструю обратную связь о бэкенд-багах до завершения более медленного UI-сьюта.
FAQ
Нужен ли Postman если использую Playwright для API-тестирования
Postman полезен для ручного исследования API: выяснить какие эндпоинты существуют, какие параметры принимают, как выглядят ответы. Когда знаешь что тестировать, пиши автоматизированную версию в Playwright. Оба инструмента имеют место.
Должен ли каждый API-эндпоинт иметь тест
Сосредоточься на важных эндпоинтах: аутентификации, основных операциях с данными и любых эндпоинтах со сложной логикой валидации. CRUD API для travel-элементов нуждается в тестах для create, read, update, delete и как минимум основных ошибочных кейсов (404, 400, 401). Не каждая комбинация входных данных.
Как тестировать загрузку файлов через API
Используй опцию multipart:
const response = await request.post('/api/upload', {
multipart: {
file: {
name: 'test.pdf',
mimeType: 'application/pdf',
buffer: Buffer.from('fake pdf content'),
}
}
});Можно ли использовать фикстуру request без page
Да. Тесты которые используют только request без page работают без открытия браузера вообще. Это чистые HTTP-тесты, работающие так же быстро как любой другой HTTP-клиент.