API 测试
简介
Playwright 可用于访问应用程序的 REST API。
有时您可能希望直接从 Node.js 发送请求到服务器,而无需加载页面并在其中运行 JavaScript 代码。以下是一些可能派上用场的场景:
- 测试您的服务器 API
- 在测试中访问 Web 应用程序前准备服务器端状态
- 在浏览器中执行某些操作后验证服务器端后置条件
所有这些都可以通过 APIRequestContext 方法实现。
编写 API 测试
APIRequestContext 可以发送各种类型的 HTTP(S) 网络请求。
以下示例演示了如何使用 Playwright 通过 GitHub API 测试 issue 创建功能。测试套件将执行以下操作:
- 在运行测试前创建一个新仓库
- 创建几个 issue 并验证服务器状态
- 运行测试后删除该仓库
配置
GitHub API 需要授权认证,因此我们将为所有测试一次性配置令牌。同时,我们也会设置 baseURL
来简化测试。你可以将这些配置放在配置文件中,或者使用 test.use()
放在测试文件中。
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
// 我们发送的所有请求都会指向这个API端点
baseURL: 'https://api.github.com',
extraHTTPHeaders: {
// 按照GitHub指南设置此请求头
'Accept': 'application/vnd.github.v3+json',
// 为所有请求添加授权令牌
// 假设环境变量中已有个人访问令牌
'Authorization': `token ${process.env.API_TOKEN}`,
},
}
});
代理配置
如果你的测试需要通过代理运行,可以在配置中指定,request
fixture 会自动获取这些配置:
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
proxy: {
server: 'http://my-proxy:8080',
username: 'user',
password: 'secret'
},
}
});
编写测试
Playwright Test 内置了 request
fixture,它会遵循我们配置的 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: 'Bug description',
}
});
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: 'Bug description'
}));
});
test('应该创建一个功能请求', async ({ request }) => {
const newIssue = await request.post(`/repos/${USER}/${REPO}/issues`, {
data: {
title: '[Feature] request 1',
body: 'Feature description',
}
});
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: 'Feature description'
}));
});
设置与清理
这些测试假设仓库已经存在。你可能希望在运行测试前创建新仓库,并在测试后删除它。可以使用 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();
});
使用请求上下文
实际上,request
fixture 在底层会调用 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}`,
}
});
})();
从 UI 测试发送 API 请求
在浏览器中运行测试时,你可能需要调用应用程序的 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 端点
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 = new URL(page.url()).pathname.split('/').pop();
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'
}));
});
复用认证状态
Web 应用通常使用基于 cookie 或基于 token 的认证方式,其中认证状态以 cookies 形式存储。Playwright 提供了 apiRequestContext.storageState() 方法,可用于从已认证的上下文中获取存储状态,然后使用该状态创建新的上下文。
存储状态可以在 BrowserContext 和 APIRequestContext 之间互换使用。您可以通过 API 调用登录后,使用已有的 cookie 创建新上下文。以下代码片段从已认证的 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 全局请求
APIRequestContext 有两种类型:
- 与 BrowserContext 关联的
- 通过 apiRequest.newContext() 创建的独立实例
主要区别在于:通过 browserContext.request 和 page.request 访问的 APIRequestContext 会从浏览器上下文中填充请求的 Cookie
头,并且如果 APIResponse 包含 Set-Cookie
头时会自动更新浏览器 cookies:
test('上下文请求会与浏览器上下文共享 cookie 存储', async ({
page,
context,
}) => {
await context.route('https://www.github.com/', async route => {
// 发送一个与浏览器上下文共享 cookie 存储的 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('=')));
// 响应会在 'Set-Cookie' 头中包含 3 个 cookies
expect(responseCookies.size).toBe(3);
const contextCookies = await context.cookies();
// 浏览器上下文已经包含了 API 响应中的所有 cookies
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 使用或更新浏览器上下文中的 cookies,可以手动创建一个新的 APIRequestContext 实例,它将拥有独立的 cookies:
test('全局上下文请求拥有独立的 cookie 存储', async ({
page,
context,
browser,
playwright
}) => {
// 创建一个拥有独立 cookie 存储的 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('=')));
// 响应会在 'Set-Cookie' 头中包含 3 个 cookies
expect(responseCookies.size).toBe(3);
const contextCookies = await context.cookies();
// 浏览器上下文不会包含来自独立 API 请求的任何 cookies
expect(contextCookies.length).toBe(0);
// 手动导出 cookie 存储
const storageState = await request.storageState();
// 创建一个新上下文并用全局请求的 cookies 初始化它
const browserContext2 = await browser.newContext({ storageState });
const contextCookies2 = await browserContext2.cookies();
// 新的浏览器上下文已经包含了 API 响应中的所有 cookies
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();
});