Когда UI-тест удаляет ресурс и проверяет интерфейс, он только подтверждает что фронтенд обновился: API-вызов мог молча завершиться с ошибкой пока UI показывал оптимистичный успех. Проверка DELETE на уровне API после этого ловит такое. APIRequestContext: встроенный HTTP-клиент Playwright, доступный через фикстуру request в том же пакете @playwright/test который уже установлен. Гайд охватывает полный рабочий процесс: GET и POST с ассёртами, аутентификация через Bearer-токен, создание тестовых данных через API перед UI-тестами, и разница между request (изолированный cookie jar) и page.request (разделяет куки браузерной сессии).
Что такое APIRequestContext и когда его использовать
APIRequestContext: встроенный HTTP-клиент Playwright. Позволяет отправлять GET, POST, PUT, PATCH, DELETE запросы, проверять ответы, работать с заголовками и куками, ассёртить статус-коды и тела ответов прямо из .spec.ts файла.
Это не замена UI-тестам. Разные задачи. UI-тест управляет браузером: кликает кнопки, заполняет формы, ждёт элементов. Тест через APIRequestContext отправляет HTTP-запросы напрямую к серверу, минуя браузер полностью. Это быстрее, надёжнее и лучше подходит для тестирования бэкенд-слоя.
Когда стоит использовать APIRequestContext вместо UI-теста:
- Тестируешь валидацию данных. Отклоняет ли API пропущенное обязательное поле?
- Тестируешь аутентификацию. Приходит ли 401 когда токен не передан?
- Тестируешь бизнес-логику которая живёт на сервере, а не в UI.
- Нужно создать тестовые данные перед UI-тестом без прохождения через форму.
- Нужно проверить побочный эффект на бэкенде после UI-действия.
Когда оставаться на UI-тестах: когда тестируешь то что пользователь реально видит и с чем взаимодействует. Рендеринг, навигацию, поведение форм, визуальную обратную связь. Оба слоя нужны в полноценном наборе тестов. Ошибка: использовать один там где явно лучше другой.
Фикстура request
Playwright предоставляет APIRequestContext через встроенную фикстуру request. Используешь её точно так же как page: объявляешь в сигнатуре функции теста и Playwright берёт настройку на себя.
import { test, expect } from '@playwright/test';
test('GET /api/items returns 200', async ({ request }) => {
const response = await request.get('https://lab.becomeqa.com/api/items');
expect(response.status()).toBe(200);
});Никакого браузерного окна. Никакого DOM. Тест-раннер отправляет HTTP-запрос, получает ответ, выполняются ассёрты. Всё это занимает меньше 100 миллисекунд на типичном соединении.
Фикстура request создаёт изолированный APIRequestContext для каждого теста: свой cookie jar, свои заголовки, никакой связи с браузерным контекстом. Изоляция намеренная: API-тесты независимы от того что делает браузер.
GET-запросы и проверка ответов
GET-тест состоит из трёх частей: отправить запрос, проверить статус, проверить тело.
import { test, expect } from '@playwright/test';
test('GET /api/items returns a valid list', async ({ request }) => {
const response = await request.get('https://lab.becomeqa.com/api/items');
// Проверка статуса
expect(response.status()).toBe(200);
expect(response.ok()).toBeTruthy(); // краткая форма: true для любого 2xx
// Парсим JSON-тело
const items = await response.json();
// Проверки структуры
expect(Array.isArray(items)).toBe(true);
expect(items.length).toBeGreaterThan(0);
// Проверяем свойства первого элемента
const first = items[0];
expect(first).toHaveProperty('id');
expect(first).toHaveProperty('destination');
expect(first).toHaveProperty('status');
});response.ok() возвращает true для любого статус-кода в диапазоне 200–299. Используй когда нужно только подтвердить успех и конкретный код не важен. response.status() используй когда важен точный код: 200, 201 и 204 означают разные вещи.
Тело можно читать также как текст или буфер:
const text = await response.text();
const buffer = await response.body(); // BufferПроверка заголовков ответа:
test('response includes JSON content-type', async ({ request }) => {
const response = await request.get('https://lab.becomeqa.com/api/items');
const headers = response.headers();
expect(headers['content-type']).toContain('application/json');
});POST-запросы с JSON-телом
POST-запросы отправляют данные на сервер. Передай payload через опцию data и Playwright автоматически сериализует его в JSON и выставит Content-Type: application/json.
test('POST /api/items creates a resource', async ({ request }) => {
const response = await request.post('https://lab.becomeqa.com/api/items', {
data: {
destination: 'Kyoto',
status: 'planned',
notes: 'Visit Arashiyama bamboo grove'
}
});
// Хорошо спроектированный API возвращает 201 Created для новых ресурсов
expect(response.status()).toBe(201);
const created = await response.json();
expect(created).toHaveProperty('id');
expect(created.destination).toBe('Kyoto');
expect(created.status).toBe('planned');
});Сохраняй возвращённый id когда нужна очистка или цепочка запросов:
test('create and then delete an item', async ({ request }) => {
const createRes = await request.post('https://lab.becomeqa.com/api/items', {
data: { destination: 'Tbilisi', status: 'planned' }
});
expect(createRes.status()).toBe(201);
const { id } = await createRes.json();
const deleteRes = await request.delete(`https://lab.becomeqa.com/api/items/${id}`);
expect(deleteRes.status()).toBe(204);
// Подтверждаем что ресурс удалён
const getRes = await request.get(`https://lab.becomeqa.com/api/items/${id}`);
expect(getRes.status()).toBe(404);
});Для form-encoded данных замени data на form:
const response = await request.post('https://lab.becomeqa.com/api/login', {
form: {
username: 'admin@becomeqa.com',
password: 'testpass123'
}
});Аутентификация: Bearer-токены и кастомные заголовки
Большинство реальных API аутентифицированы. Два наиболее распространённых паттерна: Bearer-токены и API-ключи в заголовках.
Bearer-токен: сначала логин, потом используй токен
test('authenticated request with Bearer token', async ({ request }) => {
// Шаг 1: получаем токен
const loginRes = await request.post('https://lab.becomeqa.com/api/auth/login', {
data: {
username: 'admin@becomeqa.com',
password: 'testpass123'
}
});
expect(loginRes.status()).toBe(200);
const { token } = await loginRes.json();
// Шаг 2: используем в последующих запросах
const itemsRes = await request.get('https://lab.becomeqa.com/api/items', {
headers: {
Authorization: `Bearer ${token}`
}
});
expect(itemsRes.status()).toBe(200);
});Когда один токен используется во многих тестах, выноси логин в блок beforeAll и шари токен по всему сьюту:
import { test, expect } from '@playwright/test';
let authToken: string;
test.beforeAll(async ({ request }) => {
const res = await request.post('https://lab.becomeqa.com/api/auth/login', {
data: {
username: 'admin@becomeqa.com',
password: 'testpass123'
}
});
const body = await res.json();
authToken = body.token;
});
test('GET items as authenticated user', async ({ request }) => {
const response = await request.get('https://lab.becomeqa.com/api/items', {
headers: { Authorization: `Bearer ${authToken}` }
});
expect(response.status()).toBe(200);
});API-ключ через общий заголовок: настрой один раз в playwright.config.ts
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: 'https://lab.becomeqa.com',
extraHTTPHeaders: {
'X-Api-Key': process.env.API_KEY ?? ''
}
}
});Каждый запрос который отправляет фикстура request будет включать этот заголовок автоматически. Не нужно повторять его в каждом тесте.
.env файл локально (с dotenv) и GitHub Actions secrets в CI. Доступ к process.env в Playwright одинаково работает в обоих случаях.playwright.request.newContext() для standalone API-тестов
Фикстура request удобна внутри тестов, но иногда нужен APIRequestContext снаружи тест-раннера: в файле глобального setup, в утилитарном скрипте, или когда нужен контекст со своей конфигурацией отдельно от дефолтного.
playwright.request.newContext() создаёт standalone-контекст которым управляешь явно:
// global-setup.ts
import { chromium, request } from '@playwright/test';
async function globalSetup() {
// Создаём standalone API-контекст
const apiContext = await request.newContext({
baseURL: 'https://lab.becomeqa.com',
extraHTTPHeaders: {
'Content-Type': 'application/json'
}
});
// Создаём тестовые данные до запуска тестов
await apiContext.post('/api/items', {
data: { destination: 'Lisbon', status: 'planned' }
});
// Всегда утилизируй когда закончил
await apiContext.dispose();
}
export default globalSetup;Ссылаешься на файл глобального setup в конфиге:
// playwright.config.ts
export default defineConfig({
globalSetup: './global-setup.ts',
use: {
baseURL: 'https://lab.becomeqa.com'
}
});newContext() принимает те же опции что и блок use в конфиге: baseURL, extraHTTPHeaders, httpCredentials, ignoreHTTPSErrors. Вызывай dispose() когда контекст больше не нужен: закрывает открытые соединения и очищает куки.
playwright.request.newContext() и фикстура request создают одинаковые APIRequestContext. Разница в жизненном цикле: фикстура автоматически создаётся и утилизируется для каждого теста. newContext() даёт ручное управление, полезное для глобального setup/teardown или контекстов которые охватывают несколько тестов.Комбинирование API-подготовки с UI-верификацией
Вот где APIRequestContext приносит наибольшую пользу. Самая медленная и ненадёжная часть UI-теста обычно настройка: заполнение форм, ожидание состояния, навигация через экраны только чтобы добраться до нужного сценария. Замени это API-вызовом.
API создаёт данные, UI проверяет их отображение
test('item created via API appears in the UI list', async ({ page, request }) => {
// Быстрая и надёжная настройка. Браузер не нужен.
const createRes = await request.post('https://lab.becomeqa.com/api/items', {
data: { destination: 'Porto', status: 'planned' }
});
expect(createRes.status()).toBe(201);
const { id } = await createRes.json();
// Тестируем то что важно: правильно ли UI это отображает?
await page.goto('https://lab.becomeqa.com/items');
await expect(page.getByText('Porto')).toBeVisible();
await expect(page.getByTestId(`item-${id}`)).toBeVisible();
// Очистка через API. Тоже быстрее чем кликать через UI.
await request.delete(`https://lab.becomeqa.com/api/items/${id}`);
});UI выполняет действие, API подтверждает побочный эффект
test('deleting an item via UI removes it from the database', async ({ page, request }) => {
// Создаём через API
const createRes = await request.post('https://lab.becomeqa.com/api/items', {
data: { destination: 'Valletta', status: 'planned' }
});
const { id } = await createRes.json();
// Удаляем через UI. Это то что реально тестируем.
await page.goto('https://lab.becomeqa.com/items');
await page.getByTestId(`item-${id}`).getByRole('button', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
// Проверяем состояние бэкенда, не только состояние UI
const checkRes = await request.get(`https://lab.becomeqa.com/api/items/${id}`);
expect(checkRes.status()).toBe(404);
});Второй паттерн используется редко. Когда пользователь что-то удаляет, UI может обновиться оптимистично и выглядеть корректно даже если реальный API-вызов завершился с ошибкой. Проверка на уровне API ловит такой сбой.
Переиспользуемые API-хелперы и фикстуры
Повторять request.post('/api/auth/login', ...) в каждом файле тестов: лишний шум. Построй небольшой вспомогательный класс и выставь его через кастомную фикстуру.
Сначала хелпер-класс:
// lib/api-client.ts
import { APIRequestContext } from '@playwright/test';
export class ApiClient {
constructor(private request: APIRequestContext) {}
async login(username: string, password: string): Promise<string> {
const res = await this.request.post('/api/auth/login', {
data: { username, password }
});
const { token } = await res.json();
return token;
}
async createItem(data: { destination: string; status: string; notes?: string }) {
const res = await this.request.post('/api/items', { data });
expect(res.status()).toBe(201);
return res.json();
}
async deleteItem(id: string) {
await this.request.delete(`/api/items/${id}`);
}
async getItem(id: string) {
return this.request.get(`/api/items/${id}`);
}
}Затем выставляем через кастомную фикстуру:
// fixtures.ts
import { test as base } from '@playwright/test';
import { ApiClient } from './lib/api-client';
type Fixtures = {
api: ApiClient;
};
export const test = base.extend<Fixtures>({
api: async ({ request }, use) => {
const client = new ApiClient(request);
await use(client);
}
});
export { expect } from '@playwright/test';Тесты становятся значительно читаемее:
import { test, expect } from './fixtures';
test('create and verify item', async ({ api, page }) => {
const item = await api.createItem({ destination: 'Riga', status: 'planned' });
await page.goto('https://lab.becomeqa.com/items');
await expect(page.getByText('Riga')).toBeVisible();
await api.deleteItem(item.id);
});Хелпер инкапсулирует логику запросов. Фикстура управляет жизненным циклом. Тест фокусируется на сценарии. У каждого слоя одна задача.
expect-вызовы вне класса ApiClient, кроме обязательных предусловий вроде проверки статуса 201 после создания. Ассёрты в хелперах затрудняют трассировку сбоев: стек указывает на хелпер, не на тест.request и page.request: в чём разница
Оба являются инстансами APIRequestContext. Разница в том как они обрабатывают куки и состояние сессии.
request: фикстура с изолированным контекстом. Свой cookie jar, отдельный от браузера. Не разделяет состояние с page. Если логинишься через request, браузер об этом не знает.
page.request привязан к браузерному контексту которому принадлежит page. Разделяет куки со страницей. Если пользователь залогинился через браузер, page.request несёт эти куки. Если page.request устанавливает куку, браузер её видит.
test('difference between request and page.request', async ({ page, request }) => {
// Логинимся через браузер
await page.goto('https://lab.becomeqa.com/login');
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Login' }).click();
// page.request несёт auth-куку, возвращает 200
const withCookies = await page.request.get('https://lab.becomeqa.com/api/items');
expect(withCookies.status()).toBe(200);
// фикстура request без куки, возвращает 401
const withoutCookies = await request.get('https://lab.becomeqa.com/api/items');
expect(withoutCookies.status()).toBe(401);
});Что использовать: если тест включает и браузерную сессию и API-вызовы с одинаковой аутентификацией, используй page.request. Для чистых API-тестов без браузера: фикстуру request. Если нужен полностью независимый контекст с кастомными заголовками или base URL: playwright.request.newContext().
FAQ
Нужен ли мне всё ещё Postman?
Postman хорош для исследования. Когда впервые встречаешь API и не знаешь его структуру: открой Postman, поковыряй, прочитай ответы, разберись что нужно. Как только знаешь что тестировать, пиши в Playwright. Получаешь версионный контроль, интеграцию с CI и возможность комбинировать API и UI ассёрты в одном тесте. Ничего из этого Postman не даёт.
Можно использовать APIRequestContext для тестирования GraphQL?
Да. GraphQL поверх HTTP: POST-запрос к единственному эндпоинту с JSON-телом содержащим query и опционально variables. Опция data обрабатывает это напрямую:
const response = await request.post('https://lab.becomeqa.com/graphql', {
data: {
query: `
query GetItem($id: ID!) {
item(id: $id) {
id
destination
status
}
}
`,
variables: { id: '123' }
}
});
const { data } = await response.json();
expect(data.item.destination).toBe('Kyoto');APIRequestContext автоматически следует редиректам?
Да, по умолчанию следует до 20 редиректов. Чтобы отключить следование редиректам и инспектировать ответ редиректа напрямую, передай maxRedirects: 0 в опциях запроса.
Как обрабатывать медленные или rate-limited API-тесты?
Задай кастомный таймаут в опциях запроса или увеличь timeout в playwright.config.ts для API-проекта. Для rate-limited API в тестах: создавай данные в globalSetup один раз, а не свежими в каждом тесте.
Чем response.json() отличается от response.text()?
response.json() парсит тело и возвращает JavaScript-объект. Выбрасывает исключение если тело невалидный JSON. response.text() возвращает сырую строку. Используй text() для отладки или когда эндпоинт возвращает не-JSON формат: обычный текст или XML.
→ See also: Фикстуры Playwright: от встроенных до кастомных | Глобальная настройка и очистка в Playwright | API-тестирование в Playwright: выходим за рамки UI | Продвинутое API тестирование с Playwright: паттерны для реальных проектов | Авторизация в API-тестах: API-ключи, Bearer-токены, OAuth2, JWT