Playwright можно использовать для доступа к REST API вашего приложения. Иногда может потребоваться отправить запросы на сервер напрямую из Node.js, не загружая страницу и не выполняя на ней код JavaScript. Вот несколько примеров, когда это может быть полезно:
- Тестирование вашего серверного API
- Подготовка состояния на сервере перед посещением веб-приложения в тесте
- Проверка состояния сервера после выполнения действий в браузере
Все это можно сделать с помощью методов APIRequestContext
.
Содержание:
- Написание тестов API
- Использование контекста запросов
- Отправка API-запросов из UI тестов
- Повторное использование состояния аутентификации
- Запросы контекста vs глобальные запросы
Подпишитесь на наш ТЕЛЕГРАМ КАНАЛ ПО АВТОМАТИЗАЦИИ ТЕСТИРОВАНИЯ
Написание тестов API
APIRequestContext
в Playwright позволяет отправлять любые HTTP(S)-запросы по сети. Это делает его мощным инструментом для тестирования API.
Ниже приведён пример использования Playwright для тестирования создания задач через GitHub API. В этом тестовом наборе выполняются следующие шаги:
- Создание нового репозитория перед запуском тестов
- Создание нескольких задач и проверка состояния сервера
- Удаление репозитория после завершения тестов
Конфигурация
GitHub API требует авторизации, поэтому мы настроим токен один раз для всех тестов. Также мы зададим baseURL
, чтобы упростить тесты. Эти настройки можно поместить либо в конфигурационный файл, либо в тестовый файл с помощью test.use()
.
// playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ use: { // Все запросы отправляются на этот API endpoint. baseURL: 'https://api.github.com', extraHTTPHeaders: { // Устанавливаем этот заголовок согласно рекомендациям GitHub. 'Accept': 'application/vnd.github.v3+json', // Добавляем токен авторизации ко всем запросам. // Предполагается, что личный токен доступа находится в переменных среды. 'Authorization': `token ${process.env.API_TOKEN}`, }, } });
Конфигурация прокси
Если вашим тестам нужно выполняться через прокси-сервер, вы можете указать это в настройках конфигурации. В этом случае фикстура для запросов автоматически использует эти параметры:
// playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ use: { proxy: { server: 'http://my-proxy:8080', username: 'user', password: 'secret' }, } });
Написание тестов
В Playwright Test встроена фикстура для работы с запросами, которая учитывает такие опции конфигурации, как baseURL
или extraHTTPHeaders
.
Теперь можно добавить несколько тестов, которые создадут новые задачи в репозитории.
const REPO = 'test-repo-1'; const USER = 'github-username'; test('должен создать отчёт о баге', async ({ request }) => { const newIssue = await request.post(`/repos/${USER}/${REPO}/issues`, { data: { title: '[Bug] report 1', body: 'Описание бага', } }); expect(newIssue.ok()).toBeTruthy(); const issues = await request.get(`/repos/${USER}/${REPO}/issues`); expect(issues.ok()).toBeTruthy(); expect(await issues.json()).toContainEqual(expect.objectContaining({ title: '[Bug] report 1', body: 'Описание бага' })); }); test('должен создать запрос на добавление функции', async ({ request }) => { const newIssue = await request.post(`/repos/${USER}/${REPO}/issues`, { data: { title: '[Feature] request 1', body: 'Описание функции', } }); expect(newIssue.ok()).toBeTruthy(); const issues = await request.get(`/repos/${USER}/${REPO}/issues`); expect(issues.ok()).toBeTruthy(); expect(await issues.json()).toContainEqual(expect.objectContaining({ title: '[Feature] request 1', body: 'Описание функции' })); });
Настройка и очистка
Эти тесты предполагают, что репозиторий уже существует. Вероятно, вы захотите создать новый перед запуском тестов и удалить его после завершения. Для этого используйте хуки beforeAll
и afterAll
.
test.beforeAll(async ({ request }) => { // Создание нового репозитория const response = await request.post('/user/repos', { data: { name: REPO } }); expect(response.ok()).toBeTruthy(); }); test.afterAll(async ({ request }) => { // Удаление репозитория const response = await request.delete(`/repos/${USER}/${REPO}`); expect(response.ok()).toBeTruthy(); });
Использование контекста запросов
Под капотом фикстура запроса фактически вызывает метод apiRequest.newContext()
. Вы всегда можете сделать это вручную, если хотите больше контроля. Пример ниже представляет собой автономный скрипт, который выполняет те же действия, что и хуки beforeAll
и afterAll
.
import { request } from '@playwright/test'; const REPO = 'test-repo-1'; const USER = 'github-username'; (async () => { // Создаем контекст для выполнения HTTP-запросов. const context = await request.newContext({ baseURL: 'https://api.github.com', }); // Создаем репозиторий. await context.post('/user/repos', { headers: { 'Accept': 'application/vnd.github.v3+json', // Добавляем токен доступа GitHub. 'Authorization': `token ${process.env.API_TOKEN}`, }, data: { name: REPO } }); // Удаляем репозиторий. await context.delete(`/repos/${USER}/${REPO}`, { headers: { 'Accept': 'application/vnd.github.v3+json', // Добавляем токен доступа GitHub. 'Authorization': `token ${process.env.API_TOKEN}`, } }); })();
Отправка API-запросов из UI тестов
Во время выполнения тестов в браузере вам может потребоваться сделать запросы к HTTP API вашего приложения. Это может быть полезно, если нужно подготовить состояние сервера перед выполнением теста или проверить постусловия на сервере после выполнения действий в браузере. Все это можно сделать с помощью методов APIRequestContext
.
Установка предварительных условий
Следующий тест создает новую задачу через API. Затем переходит на страницу со списком всех задач в проекте, чтобы проверить, что она отображается вверху списка.
import { test, expect } from '@playwright/test'; const REPO = 'test-repo-1'; const USER = 'github-username'; // Контекст запроса используется во всех тестах файла. let apiContext; test.beforeAll(async ({ playwright }) => { apiContext = await playwright.request.newContext({ // Все запросы отправляются на этот эндпоинт API. baseURL: 'https://api.github.com', extraHTTPHeaders: { // Устанавливаем заголовок согласно рекомендациям GitHub. 'Accept': 'application/vnd.github.v3+json', // Добавляем токен авторизации ко всем запросам. // Предполагается, что токен доступа находится в переменных среды. 'Authorization': `token ${process.env.API_TOKEN}`, }, }); }); test.afterAll(async () => { // Удаляем все ответы. await apiContext.dispose(); }); test('последняя созданная задача должна быть первой в списке', async ({ page }) => { const newIssue = await apiContext.post(`/repos/${USER}/${REPO}/issues`, { data: { title: '[Feature] request 1', } }); expect(newIssue.ok()).toBeTruthy(); await page.goto(`https://github.com/${USER}/${REPO}/issues`); const firstIssue = page.locator(`a[data-hovercard-type='issue']`).first(); await expect(firstIssue).toHaveText('[Feature] request 1'); });
Проверка постусловий
Следующий тест создает новую задачу через пользовательский интерфейс в браузере, а затем проверяет, была ли она создана, с помощью API:
import { test, expect } from '@playwright/test'; const REPO = 'test-repo-1'; const USER = 'github-username'; // Контекст запроса используется во всех тестах файла. let apiContext; test.beforeAll(async ({ playwright }) => { apiContext = await playwright.request.newContext({ // Все запросы отправляются на этот API endpoint. baseURL: 'https://api.github.com', extraHTTPHeaders: { // Устанавливаем заголовок согласно рекомендациям GitHub. 'Accept': 'application/vnd.github.v3+json', // Добавляем токен авторизации ко всем запросам. 'Authorization': `token ${process.env.API_TOKEN}`, }, }); }); test.afterAll(async () => { // Удаляем все ответы. await apiContext.dispose(); }); test('последняя созданная задача должна быть на сервере', async ({ page }) => { await page.goto(`https://github.com/${USER}/${REPO}/issues`); await page.getByText('New Issue').click(); await page.getByRole('textbox', { name: 'Title' }).fill('Bug report 1'); await page.getByRole('textbox', { name: 'Comment body' }).fill('Bug description'); await page.getByText('Submit new issue').click(); const issueId = page.url().substr(page.url().lastIndexOf('/')); const newIssue = await apiContext.get( `https://api.github.com/repos/${USER}/${REPO}/issues/${issueId}` ); expect(newIssue.ok()).toBeTruthy(); expect(newIssue.json()).toEqual(expect.objectContaining({ title: 'Bug report 1' })); });
Повторное использование состояния аутентификации
В веб-приложениях обычно используется аутентификация на основе куки (cookie) или токенов, где аутентифицированное состояние хранится в виде куки-файлов. В Playwright есть метод apiRequestContext.storageState()
, который позволяет получить состояние хранилища из аутентифицированного контекста, а затем создать новые контексты с этим состоянием.
Состояние хранилища можно использовать как для BrowserContext
, так и для APIRequestContext
. Это позволяет выполнить вход через API-запросы, а затем создать новый контекст, уже содержащий куки-файлы. В следующем примере показано, как получить состояние из аутентифицированного контекста APIRequestContext
и создать новый BrowserContext
с этим состоянием.
const requestContext = await request.newContext({ httpCredentials: { username: 'user', // имя пользователя password: 'passwd' // пароль } }); await requestContext.get(`https://api.example.com/login`); // Сохранить состояние хранилища в файл. await requestContext.storageState({ path: 'state.json' }); // Создать новый контекст с сохраненным состоянием хранилища. const context = await browser.newContext({ storageState: 'state.json' });
Примечание редакции: вас также может заинтересовать статья “Лучшие практики автоматизации тестирования”.
Запросы контекста vs глобальные запросы
Запросы, связанные с контекстом браузера (запросы контекста) и глобальные запросы описывают два разных способа работы с запросами API в Playwright, которые касаются того, как обрабатываются куки и аутентификация. Можно сказать, что запросы контекста используются для тесной интеграции с состоянием браузера, а глобальные — для изолированной работы с API без привязки к браузеру.
В Playwright есть два типа APIRequestContext
:
- ассоциированный с
BrowserContext
- изолированный экземпляр, созданный с помощью
apiRequest.newContext()
Основное различие в том, что APIRequestContext
, доступный через browserContext.request
и page.request
, автоматически добавляет заголовок Cookie из контекста браузера и обновляет куки браузера, если в ответе API есть заголовок Set-Cookie
.
Пример, где запрос контекста делится куками с контекстом браузера:
test('запрос контекста будет использовать общие куки с контекстом браузера', async ({ page, context }) => { await context.route('https://www.github.com/', async route => { // Отправка API-запроса, который использует куки браузера. const response = await context.request.fetch(route.request()); const responseHeaders = response.headers(); // Ответ будет содержать заголовок 'Set-Cookie'. const responseCookies = new Map(responseHeaders['set-cookie'] .split('\n') .map(c => c.split(';', 2)[0].split('='))); // В ответе будет 3 куки в заголовке 'Set-Cookie'. expect(responseCookies.size).toBe(3); const contextCookies = await context.cookies(); // Контекст браузера уже будет содержать все куки из API-ответа. expect(new Map(contextCookies.map(({ name, value }) => [name, value]) )).toEqual(responseCookies); await route.fulfill({ response, headers: { ...responseHeaders, foo: 'bar' }, }); }); await page.goto('https://www.github.com/'); });
Изолированные запросы
Если вы не хотите, чтобы APIRequestContext
использовал и обновлял куки из контекста браузера, можно вручную создать новый изолированный экземпляр APIRequestContext
, который будет иметь свои собственные куки:
test('глобальный контекст запроса имеет изолированное хранилище куки', async ({ page, context, browser, playwright }) => { // Создать новый экземпляр APIRequestContext с изолированными куками. const request = await playwright.request.newContext(); await context.route('https://www.github.com/', async route => { const response = await request.fetch(route.request()); const responseHeaders = response.headers(); const responseCookies = new Map(responseHeaders['set-cookie'] .split('\n') .map(c => c.split(';', 2)[0].split('='))); // В ответе будет 3 куки в заголовке 'Set-Cookie'. expect(responseCookies.size).toBe(3); const contextCookies = await context.cookies(); // Контекст браузера не будет содержать куки из изолированного API-запроса. expect(contextCookies.length).toBe(0); // Экспортировать куки вручную. const storageState = await request.storageState(); // Создать новый контекст и инициализировать его куками из глобального запроса. const browserContext2 = await browser.newContext({ storageState }); const contextCookies2 = await browserContext2.cookies(); // Новый контекст браузера будет содержать все куки из ответа API. expect( new Map(contextCookies2.map(({ name, value }) => [name, value])) ).toEqual(responseCookies); await route.fulfill({ response, headers: { ...responseHeaders, foo: 'bar' }, }); }); await page.goto('https://www.github.com/'); await request.dispose(); });
Этот пример демонстрирует, как работать с разными типами запросов, как разделять или изолировать куки между API и контекстом браузера.
Перевод статьи «API testing».