API 測試
簡介
Playwright 可用於存取您應用程式的 REST API。
有時您可能想要直接從 Node.js 向伺服器發送請求,而不載入頁面並在其中執行 js 程式碼。在以下情況下可能會很有用:
- 測試您的伺服器 API。
- 在測試中存取 Web 應用程式之前準備伺服器端狀態。
- 在瀏覽器中執行某些操作後驗證伺服器端後置條件。
所有這些都可以透過 [APIRequestContext
] 方法來實現。
撰寫 API 測試
[APIRequestContext
] 可以透過網路發送各種 HTTP(S) 請求。
以下範例示範如何使用 Playwright 透過 GitHub API 測試問題的建立。測試套件將執行以下操作:
- 在執行測試前建立新的儲存庫。
- 建立幾個問題並驗證伺服器狀態。
- 在執行測試後刪除儲存庫。
設定
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
佈置將自動選取它:
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
proxy: {
server: 'http://my-proxy:8080',
username: 'user',
password: 'secret'
},
}
});
撰寫測試
Playwright Test 附帶內建的 request
佈置,它尊重我們指定的設定選項,如 baseURL
或 extraHTTPHeaders
,並準備發送一些請求。
現在我們可以新增一些測試,這些測試將在儲存庫中建立新問題。
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'
}));
});
設定和清理
這些測試假設儲存庫存在。您可能希望在執行測試前建立新的儲存庫,並在測試後刪除它。使用 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
佈置 實際上會呼叫 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('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';
// 請求上下文被檔案中的所有測試重複使用。
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('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 = 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 或基於令牌的身份驗證,其中認證狀態儲存為 cookies。Playwright 提供 apiRequestContext.storageState() 方法,可用於從已認證的上下文中擷取儲存狀態,然後使用該狀態建立新的上下文。
儲存狀態在 [BrowserContext
] 和 [APIRequestContext
] 之間是可互換的。您可以使用它透過 API 呼叫登入,然後建立一個已包含 cookies 的新上下文。以下程式碼片段從已認證的 [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' });
Context request vs 全域 request
有兩種類型的 [APIRequestContext
]:
- 與 [
BrowserContext
] 關聯的 - 獨立實例,透過 apiRequest.newContext() 建立
主要差異是透過 browserContext.request 和 page.request 存取的 [APIRequestContext
] 將會從瀏覽器上下文填入請求的 Cookie
標頭,並且如果 [APIResponse
] 有 Set-Cookie
標頭,將自動更新瀏覽器 cookies:
test('context request will share cookie storage with its browser context', 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('global context request has isolated cookie storage', 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();
});