APIテスト
はじめに
Playwrightは、アプリケーションのREST APIにアクセスするために使用できます。
ページを読み込んだり、その中でJavaScriptコードを実行したりすることなく、Node.jsから直接サーバーにリクエストを送信したい場合があります。これは、次のような場合に役立つことがあります:
- サーバーAPIをテストする。
- テストでWebアプリケーションにアクセスする前に、サーバー側の状態を準備する。
- ブラウザで何らかのアクションを実行した後、サーバー側の事後条件を検証する。
これらすべては、APIRequestContext メソッドを介して実現できます。
APIテストの作成
APIRequestContext は、あらゆる種類のHTTP(S)リクエストをネットワーク経由で送信できます。
以下の例は、GitHub API を介した課題の作成をPlaywrightでテストする方法を示しています。テストスイートは以下を実行します:
- テストを実行する前に新しいリポジトリを作成する。
- いくつかの課題を作成し、サーバーの状態を検証する。
- テスト実行後にリポジトリを削除する。
設定
GitHub APIは認証を必要とするため、すべてのテストに対してトークンを一度設定します。同時に、テストを簡素化するために baseURL
も設定します。これらは設定ファイルに入れるか、test.use()
を使用してテストファイルに入れることができます。
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}`,
},
}
});
プロキシ設定
テストをプロキシの背後で実行する必要がある場合は、設定でこれを指定でき、request
フィクスチャが自動的にそれを検出します。
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
proxy: {
server: 'http://my-proxy:8080',
username: 'user',
password: 'secret'
},
}
});
テストの作成
Playwright Testには、指定した baseURL
や extraHTTPHeaders
などの設定オプションを尊重し、リクエストを送信できる組み込みの request
フィクスチャが付属しています。
これで、リポジトリに新しい課題を作成するいくつかのテストを追加できます。
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 }) => {
// 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()
を呼び出します。より多くの制御が必要な場合は、常に手動でそれを行うことができます。以下は、上記の beforeAll
と afterAll
と同じことを行うスタンドアロンスクリプトです。
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 = 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アプリケーションは、認証状態がクッキーとして保存されるクッキーベースまたはトークンベースの認証を使用します。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`);
// 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' });
コンテキストリクエストとグローバルリクエスト
APIRequestContext には2つのタイプがあります
- BrowserContext に関連付けられたもの
apiRequest.newContext()
を介して作成された分離されたインスタンス
主な違いは、browserContext.request
および page.request
を介してアクセスできる APIRequestContext は、ブラウザコンテキストからリクエストの Cookie
ヘッダーを設定し、APIResponse に Set-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 がブラウザコンテキストからクッキーを使用および更新することを望まない場合、独自の分離されたクッキーを持つ APIRequestContext の新しいインスタンスを手動で作成できます。
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();
});