В данной статье рассматриваются методы тестирования API с использованием Playwright, включая создание запросов, обработку аутентификации и проверку ответов сервера.
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 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 }) => { 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».