跳到主要內容

API 測試

簡介

Playwright 可以用於存取應用程式的 REST API。

有時您可能想要直接從 Node.js 發送請求到伺服器,而無需載入頁面並在其中執行 js 程式碼。以下是一些可能有用的範例

  • 測試您的伺服器 API。
  • 在測試中訪問 Web 應用程式之前,準備伺服器端狀態。
  • 在瀏覽器中執行某些動作後,驗證伺服器端後置條件。

所有這些都可以通過 APIRequestContext 方法實現。

編寫 API 測試

APIRequestContext 可以通過網路發送各種 HTTP(S) 請求。

以下範例示範如何使用 Playwright 通過 GitHub API 測試問題建立。測試套件將執行以下操作

  • 在執行測試之前建立一個新的儲存庫。
  • 建立一些問題並驗證伺服器狀態。
  • 在執行測試後刪除儲存庫。

設定

GitHub API 需要授權,因此我們將為所有測試設定一次權杖。同時,我們還將設定 baseURL 以簡化測試。您可以將它們放在設定檔中,或使用 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 設定

如果您的測試需要通過 Proxy 執行,您可以在設定中指定它,並且 request 夾具將自動檢測到它

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

編寫測試

Playwright Test 隨附內建的 request 夾具,它會遵循諸如我們指定的 baseURLextraHTTPHeaders 等設定選項,並準備好發送一些請求。

現在我們可以新增一些將在儲存庫中建立新問題的測試。

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'
}));
});

設定與拆卸

這些測試假設儲存庫存在。您可能希望在執行測試之前建立一個新的儲存庫,並在之後刪除它。使用 beforeAllafterAll hook 來實現。

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();
});

使用請求上下文

在幕後,request 夾具 實際上會調用 apiRequest.newContext()。如果您想要更多控制權,您可以隨時手動執行此操作。以下是一個獨立的腳本,它執行與上面 beforeAllafterAll 相同的操作。

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 請求

在瀏覽器內執行測試時,您可能想要呼叫應用程式的 HTTP API。如果您需要在執行測試之前準備伺服器狀態,或者在瀏覽器中執行某些動作後檢查伺服器上的一些後置條件,這可能會很有幫助。所有這些都可以通過 APIRequestContext 方法實現。

建立先決條件

以下測試通過 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 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');
});

驗證後置條件

以下測試通過瀏覽器中的使用者介面建立一個新問題,然後使用 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'
}));
});

重複使用身份驗證狀態

Web 應用程式使用基於 Cookie 或基於權杖的身份驗證,其中經過身份驗證的狀態儲存為 Cookie。Playwright 提供了 apiRequestContext.storageState() 方法,可用於從經過身份驗證的上下文中檢索儲存狀態,然後使用該狀態建立新的上下文。

儲存狀態在 BrowserContextAPIRequestContext 之間是可互換的。您可以使用它通過 API 呼叫登入,然後建立一個 Cookie 已存在的新上下文。以下程式碼片段從經過身份驗證的 APIRequestContext 檢索狀態,並建立一個具有該狀態的新 BrowserContext

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' });

上下文請求 vs 全域請求

有兩種類型的 APIRequestContext

主要區別在於通過 browserContext.requestpage.request 訪問的 APIRequestContext 將從瀏覽器上下文中填充請求的 Cookie 標頭,並且如果 APIResponse 具有 Set-Cookie 標頭,則會自動更新瀏覽器 Cookie

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

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();
});