Skip to main content

API 测试

介绍

¥Introduction

Playwright 可用于访问应用的 REST API。

¥Playwright can be used to get access to the REST API of your application.

有时你可能希望直接从 Node.js 向服务器发送请求,而不需要加载页面并在其中运行 js 代码。一些它可能派上用场的例子:

¥Sometimes you may want to send requests to the server directly from Node.js without loading a page and running js code in it. A few examples where it may come in handy:

  • 测试你的服务器 API。

    ¥Test your server API.

  • 在测试中访问 Web 应用之前准备服务器端状态。

    ¥Prepare server side state before visiting the web application in a test.

  • 在浏览器中运行某些操作后验证服务器端后置条件。

    ¥Validate server side post-conditions after running some actions in the browser.

所有这些都可以通过 APIRequestContext 方法来实现。

¥All of that could be achieved via APIRequestContext methods.

编写 API 测试

¥Writing API Test

APIRequestContext 可以通过网络发送各种 HTTP(S)请求。

¥APIRequestContext can send all kinds of HTTP(S) requests over network.

以下示例演示如何使用 Playwright 通过 GitHub API 测试问题创建。测试套件将执行以下操作:

¥The following example demonstrates how to use Playwright to test issues creation via GitHub API. The test suite will do the following:

  • 在运行测试之前创建一个新的存储库。

    ¥Create a new repository before running tests.

  • 创建一些问题并验证服务器状态。

    ¥Create a few issues and validate server state.

  • 运行测试后删除存储库。

    ¥Delete the repository after running tests.

配置

¥Configuration

GitHub API 需要授权,因此我们将为所有测试配置一次令牌。同时,我们还将设置 baseURL 以简化测试。你可以将它们放入配置文件中,也可以将它们放入带有 test.use() 的测试文件中。

¥GitHub API requires authorization, so we'll configure the token once for all tests. While at it, we'll also set the baseURL to simplify the tests. You can either put them in the configuration file, or in the test file with test.use().

playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
// All requests we send go to this API endpoint.
baseURL: 'https://api.github.com',
extraHTTPHeaders: {
// We set this header per GitHub guidelines.
'Accept': 'application/vnd.github.v3+json',
// Add authorization token to all requests.
// Assuming personal access token available in the environment.
'Authorization': `token ${process.env.API_TOKEN}`,
},
}
});

代理配置

¥Proxy configuration

如果你的测试需要在代理后面运行,你可以在配置中指定这一点,request 夹具将自动选择它:

¥If your tests need to run behind a proxy, you can specify this in the config and the request fixture will pick it up automatically:

playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
proxy: {
server: 'http://my-proxy:8080',
username: 'user',
password: 'secret'
},
}
});

编写测试

¥Writing tests

Playwright Test 附带内置 request 装置,它尊重我们指定的 baseURLextraHTTPHeaders 等配置选项,并准备发送一些请求。

¥Playwright Test comes with the built-in request fixture that respects configuration options like baseURL or extraHTTPHeaders we specified and is ready to send some requests.

现在我们可以添加一些测试,这些测试将在存储库中创建新问题。

¥Now we can add a few tests that will create new issues in the repository.

const REPO = 'test-repo-1';
const USER = 'github-username';

test('should create a bug report', 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('should create a feature request', 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'
}));
});

设置和拆卸

¥Setup and teardown

这些测试假设存储库存在。你可能想在运行测试之前创建一个新的测试,然后将其删除。为此使用 beforeAllafterAll 钩子。

¥These tests assume that repository exists. You probably want to create a new one before running tests and delete it afterwards. Use beforeAll and afterAll hooks for that.

test.beforeAll(async ({ request }) => {
// Create a new repository
const response = await request.post('/user/repos', {
data: {
name: REPO
}
});
expect(response.ok()).toBeTruthy();
});

test.afterAll(async ({ request }) => {
// Delete the repository
const response = await request.delete(`/repos/${USER}/${REPO}`);
expect(response.ok()).toBeTruthy();
});

使用请求上下文

¥Using request context

在幕后,request 夹具 实际上会调用 apiRequest.newContext()。如果你想要更多控制权,你可以随时手动执行此操作。下面是一个独立脚本,其作用与上面的 beforeAllafterAll 相同。

¥Behind the scenes, request fixture will actually call apiRequest.newContext(). You can always do that manually if you'd like more control. Below is a standalone script that does the same as beforeAll and afterAll from above.

import { request } from '@playwright/test';
const REPO = 'test-repo-1';
const USER = 'github-username';

(async () => {
// Create a context that will issue http requests.
const context = await request.newContext({
baseURL: 'https://api.github.com',
});

// Create a repository.
await context.post('/user/repos', {
headers: {
'Accept': 'application/vnd.github.v3+json',
// Add GitHub personal access token.
'Authorization': `token ${process.env.API_TOKEN}`,
},
data: {
name: REPO
}
});

// Delete a repository.
await context.delete(`/repos/${USER}/${REPO}`, {
headers: {
'Accept': 'application/vnd.github.v3+json',
// Add GitHub personal access token.
'Authorization': `token ${process.env.API_TOKEN}`,
}
});
})();

从 UI 测试发送 API 请求

¥Sending API requests from UI tests

在浏览器内运行测试时,你可能想要调用应用的 HTTP API。如果你需要在运行测试之前准备服务器状态或在浏览器中执行某些操作后检查服务器上的某些后置条件,这可能会有所帮助。所有这些都可以通过 APIRequestContext 方法来实现。

¥While running tests inside browsers you may want to make calls to the HTTP API of your application. It may be helpful if you need to prepare server state before running a test or to check some postconditions on the server after performing some actions in the browser. All of that could be achieved via APIRequestContext methods.

建立前提条件

¥Establishing preconditions

以下测试通过 API 创建一个新问题,然后导航到项目中所有问题的列表以检查它是否出现在列表顶部。

¥The following test creates a new issue via API and then navigates to the list of all issues in the project to check that it appears at the top of the list.

import { test, expect } from '@playwright/test';

const REPO = 'test-repo-1';
const USER = 'github-username';

// Request context is reused by all tests in the file.
let apiContext;

test.beforeAll(async ({ playwright }) => {
apiContext = await playwright.request.newContext({
// All requests we send go to this API endpoint.
baseURL: 'https://api.github.com',
extraHTTPHeaders: {
// We set this header per GitHub guidelines.
'Accept': 'application/vnd.github.v3+json',
// Add authorization token to all requests.
// Assuming personal access token available in the environment.
'Authorization': `token ${process.env.API_TOKEN}`,
},
});
});

test.afterAll(async ({ }) => {
// Dispose all responses.
await apiContext.dispose();
});

test('last created issue should be first in the list', 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');
});

验证后置条件

¥Validating postconditions

以下测试通过浏览器中的用户界面创建一个新问题,然后检查它是否是通过 API 创建的:

¥The following test creates a new issue via user interface in the browser and then uses checks if it was created via API:

import { test, expect } from '@playwright/test';

const REPO = 'test-repo-1';
const USER = 'github-username';

// Request context is reused by all tests in the file.
let apiContext;

test.beforeAll(async ({ playwright }) => {
apiContext = await playwright.request.newContext({
// All requests we send go to this API endpoint.
baseURL: 'https://api.github.com',
extraHTTPHeaders: {
// We set this header per GitHub guidelines.
'Accept': 'application/vnd.github.v3+json',
// Add authorization token to all requests.
// Assuming personal access token available in the environment.
'Authorization': `token ${process.env.API_TOKEN}`,
},
});
});

test.afterAll(async ({ }) => {
// Dispose all responses.
await apiContext.dispose();
});

test('last created issue should be on the server', 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'
}));
});

重用身份验证状态

¥Reusing authentication state

Web 应用使用基于 cookie 或基于令牌的身份验证,其中经过身份验证的状态存储为 cookies。Playwright 提供了 apiRequestContext.storageState() 方法,可用于从经过身份验证的上下文中检索存储状态,然后使用该状态创建新的上下文。

¥Web apps use cookie-based or token-based authentication, where authenticated state is stored as cookies. Playwright provides apiRequestContext.storageState() method that can be used to retrieve storage state from an authenticated context and then create new contexts with that state.

存储状态可以在 BrowserContextAPIRequestContext 之间互换。你可以使用它通过 API 调用登录,然后创建一个已存在 cookie 的新上下文。以下代码片段从经过身份验证的 APIRequestContext 检索状态,并使用该状态创建一个新的 BrowserContext

¥Storage state is interchangeable between BrowserContext and APIRequestContext. You can use it to log in via API calls and then create a new context with cookies already there. The following code snippet retrieves state from an authenticated APIRequestContext and creates a new BrowserContext with that state.

const requestContext = await request.newContext({
httpCredentials: {
username: 'user',
password: 'passwd'
}
});
await requestContext.get(`https://api.example.com/login`);
// Save storage state into the file.
await requestContext.storageState({ path: 'state.json' });

// Create a new context with the saved storage state.
const context = await browser.newContext({ storageState: 'state.json' });

上下文请求与全局请求

¥Context request vs global request

APIRequestContext 有两种类型:

¥There are two types of APIRequestContext:

主要区别在于,可通过 browserContext.requestpage.request 访问的 APIRequestContext 将从浏览器上下文中填充请求的 Cookie 标头,并且如果 APIResponse 具有 Set-Cookie 标头,则将自动更新浏览器 cookie:

¥The main difference is that APIRequestContext accessible via browserContext.request and page.request will populate request's Cookie header from the browser context and will automatically update browser cookies if APIResponse has Set-Cookie header:

test('context request will share cookie storage with its browser context', async ({
page,
context,
}) => {
await context.route('https://www.github.com/', async route => {
// Send an API request that shares cookie storage with the browser context.
const response = await context.request.fetch(route.request());
const responseHeaders = response.headers();

// The response will have 'Set-Cookie' header.
const responseCookies = new Map(responseHeaders['set-cookie']
.split('\n')
.map(c => c.split(';', 2)[0].split('=')));
// The response will have 3 cookies in 'Set-Cookie' header.
expect(responseCookies.size).toBe(3);
const contextCookies = await context.cookies();
// The browser context will already contain all the cookies from the API response.
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 使用和更新浏览器上下文中的 cookie,你可以手动创建 APIRequestContext 的新实例,该实例将拥有自己的独立 cookie:

¥If you don't want APIRequestContext to use and update cookies from the browser context, you can manually create a new instance of APIRequestContext which will have its own isolated cookies:

test('global context request has isolated cookie storage', async ({
page,
context,
browser,
playwright
}) => {
// Create a new instance of APIRequestContext with isolated cookie storage.
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('=')));
// The response will have 3 cookies in 'Set-Cookie' header.
expect(responseCookies.size).toBe(3);
const contextCookies = await context.cookies();
// The browser context will not have any cookies from the isolated API request.
expect(contextCookies.length).toBe(0);

// Manually export cookie storage.
const storageState = await request.storageState();
// Create a new context and initialize it with the cookies from the global request.
const browserContext2 = await browser.newContext({ storageState });
const contextCookies2 = await browserContext2.cookies();
// The new browser context will already contain all the cookies from the API response.
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();
});