認証
はじめに
Playwrightは、「ブラウザコンテキスト」と呼ばれる分離された環境でテストを実行します。この分離モデルにより、再現性が向上し、連鎖的なテスト失敗が防止されます。テストは既存の認証済み状態を読み込むことができます。これにより、すべてのテストで認証する必要がなくなり、テストの実行が高速化されます。
コアコンセプト
どの認証戦略を選択するかにかかわらず、認証済みのブラウザ状態をファイルシステムに保存することになるでしょう。
playwright/.auth
ディレクトリを作成し、それを.gitignore
に追加することをお勧めします。認証ルーチンは認証済みブラウザ状態を生成し、このplaywright/.auth
ディレクトリ内のファイルに保存します。その後、テストはこの状態を再利用し、既に認証された状態で開始します。
ブラウザの状態ファイルには、あなたやあなたのテストアカウントをなりすますために使用される可能性のある機密性の高いクッキーやヘッダーが含まれている場合があります。これらをプライベートまたはパブリックリポジトリにチェックインすることは強くお勧めしません。
- Bash
- PowerShell
- Batch
mkdir -p playwright/.auth
echo $'\nplaywright/.auth' >> .gitignore
New-Item -ItemType Directory -Force -Path playwright\.auth
Add-Content -path .gitignore "`r`nplaywright/.auth"
md playwright\.auth
echo. >> .gitignore
echo "playwright/.auth" >> .gitignore
基本: すべてのテストで共有アカウント
これは、**サーバーサイドの状態がない**テストに**推奨される**アプローチです。**セットアッププロジェクト**で一度認証し、認証状態を保存し、その後それを再利用して各テストを既に認証された状態で起動します。
使用するタイミング
- すべてのテストが同じアカウントで同時に実行され、互いに影響を与えない場合。
使用しないタイミング
- テストがサーバーサイドの状態を変更する場合。例えば、あるテストが設定ページのレンダリングをチェックしている間に、別のテストが設定を変更しており、それらを並行して実行する場合などです。この場合、テストは異なるアカウントを使用する必要があります。
- 認証がブラウザ固有の場合。
詳細
他のすべてのテストのために認証済みのブラウザ状態を準備するtests/auth.setup.ts
を作成します。
import { test as setup, expect } from '@playwright/test';
import path from 'path';
const authFile = path.join(__dirname, '../playwright/.auth/user.json');
setup('authenticate', async ({ page }) => {
// Perform authentication steps. Replace these actions with your own.
await page.goto('https://github.com/login');
await page.getByLabel('Username or email address').fill('username');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
// Wait until the page receives the cookies.
//
// Sometimes login flow sets cookies in the process of several redirects.
// Wait for the final URL to ensure that the cookies are actually set.
await page.waitForURL('https://github.com/');
// Alternatively, you can wait until the page reaches a state where all cookies are set.
await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
// End of authentication steps.
await page.context().storageState({ path: authFile });
});
設定に新しいsetup
プロジェクトを作成し、すべてのテストプロジェクトの依存関係として宣言します。このプロジェクトは常にすべてのテストの前に実行され、認証を行います。すべてのテストプロジェクトは、認証された状態をstorageState
として使用する必要があります。
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
// Setup project
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
// Use prepared auth state.
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
// Use prepared auth state.
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
設定でstorageState
を指定したため、テストはすでに認証された状態で開始されます。
import { test } from '@playwright/test';
test('test', async ({ page }) => {
// page is authenticated
});
保存された状態が期限切れになったら削除する必要があることに注意してください。テスト実行間で状態を保持する必要がない場合は、ブラウザの状態をtestProject.outputDirの下に書き込みます。これは、すべてのテスト実行前に自動的にクリーンアップされます。
UIモードでの認証
UIモードでは、テスト速度を向上させるため、デフォルトでsetup
プロジェクトを実行しません。既存の認証が期限切れになった場合は、随時auth.setup.ts
を手動で実行して認証することをお勧めします。
まず、フィルターでsetup
プロジェクトを有効にし、次にauth.setup.ts
ファイルの横にある三角ボタンをクリックし、その後再度フィルターでsetup
プロジェクトを無効にします。
中程度: 並列ワーカーごとに1つのアカウント
これは、**サーバーサイドの状態を変更する**テストに**推奨される**アプローチです。Playwrightでは、ワーカープロセスが並列で実行されます。このアプローチでは、各並列ワーカーは一度認証されます。ワーカーによって実行されるすべてのテストは、同じ認証状態を再利用します。したがって、並列ワーカーごとに1つ、複数のテストアカウントが必要になります。
使用するタイミング
- テストが共有サーバーサイドの状態を変更する場合。例えば、あるテストが設定ページのレンダリングをチェックしている間に、別のテストが設定を変更している場合などです。
使用しないタイミング
- テストが共有サーバーサイドの状態を変更しない場合。この場合、すべてのテストで単一の共有アカウントを使用できます。
詳細
各ワーカープロセスごとに、一意のアカウントで一度認証を行います。
playwright/fixtures.ts
ファイルを作成し、ワーカーごとに一度認証するためにstorageState
フィクスチャをオーバーライドします。testInfo.parallelIndexを使用して、ワーカーを区別します。
import { test as baseTest, expect } from '@playwright/test';
import fs from 'fs';
import path from 'path';
export * from '@playwright/test';
export const test = baseTest.extend<{}, { workerStorageState: string }>({
// Use the same storage state for all tests in this worker.
storageState: ({ workerStorageState }, use) => use(workerStorageState),
// Authenticate once per worker with a worker-scoped fixture.
workerStorageState: [async ({ browser }, use) => {
// Use parallelIndex as a unique identifier for each worker.
const id = test.info().parallelIndex;
const fileName = path.resolve(test.info().project.outputDir, `.auth/${id}.json`);
if (fs.existsSync(fileName)) {
// Reuse existing authentication state if any.
await use(fileName);
return;
}
// Important: make sure we authenticate in a clean environment by unsetting storage state.
const page = await browser.newPage({ storageState: undefined });
// Acquire a unique account, for example create a new one.
// Alternatively, you can have a list of precreated accounts for testing.
// Make sure that accounts are unique, so that multiple team members
// can run tests at the same time without interference.
const account = await acquireAccount(id);
// Perform authentication steps. Replace these actions with your own.
await page.goto('https://github.com/login');
await page.getByLabel('Username or email address').fill(account.username);
await page.getByLabel('Password').fill(account.password);
await page.getByRole('button', { name: 'Sign in' }).click();
// Wait until the page receives the cookies.
//
// Sometimes login flow sets cookies in the process of several redirects.
// Wait for the final URL to ensure that the cookies are actually set.
await page.waitForURL('https://github.com/');
// Alternatively, you can wait until the page reaches a state where all cookies are set.
await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
// End of authentication steps.
await page.context().storageState({ path: fileName });
await page.close();
await use(fileName);
}, { scope: 'worker' }],
});
これで、各テストファイルは@playwright/test
ではなく、フィクスチャファイルからtest
をインポートする必要があります。設定に変更は必要ありません。
// Important: import our fixtures.
import { test, expect } from '../playwright/fixtures';
test('test', async ({ page }) => {
// page is authenticated
});
高度なシナリオ
APIリクエストによる認証
使用するタイミング
- あなたのWebアプリケーションが、アプリのUIを操作するよりも簡単/高速なAPI経由での認証をサポートしている場合。
詳細
私たちはAPIRequestContextを使用してAPIリクエストを送信し、その後通常通り認証された状態を保存します。
import { test as setup } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ request }) => {
// Send authentication request. Replace with your own.
await request.post('https://github.com/login', {
form: {
'user': 'user',
'password': 'password'
}
});
await request.storageState({ path: authFile });
});
あるいは、ワーカーフィクスチャで
import { test as baseTest, request } from '@playwright/test';
import fs from 'fs';
import path from 'path';
export * from '@playwright/test';
export const test = baseTest.extend<{}, { workerStorageState: string }>({
// Use the same storage state for all tests in this worker.
storageState: ({ workerStorageState }, use) => use(workerStorageState),
// Authenticate once per worker with a worker-scoped fixture.
workerStorageState: [async ({}, use) => {
// Use parallelIndex as a unique identifier for each worker.
const id = test.info().parallelIndex;
const fileName = path.resolve(test.info().project.outputDir, `.auth/${id}.json`);
if (fs.existsSync(fileName)) {
// Reuse existing authentication state if any.
await use(fileName);
return;
}
// Important: make sure we authenticate in a clean environment by unsetting storage state.
const context = await request.newContext({ storageState: undefined });
// Acquire a unique account, for example create a new one.
// Alternatively, you can have a list of precreated accounts for testing.
// Make sure that accounts are unique, so that multiple team members
// can run tests at the same time without interference.
const account = await acquireAccount(id);
// Send authentication request. Replace with your own.
await context.post('https://github.com/login', {
form: {
'user': 'user',
'password': 'password'
}
});
await context.storageState({ path: fileName });
await context.dispose();
await use(fileName);
}, { scope: 'worker' }],
});
複数のサインイン済みロール
使用するタイミング
- エンドツーエンドテストで複数のロールを持っているが、すべてのアカウントをテスト全体で再利用できる場合。
詳細
セットアッププロジェクトで複数回認証を行います。
import { test as setup, expect } from '@playwright/test';
const adminFile = 'playwright/.auth/admin.json';
setup('authenticate as admin', async ({ page }) => {
// Perform authentication steps. Replace these actions with your own.
await page.goto('https://github.com/login');
await page.getByLabel('Username or email address').fill('admin');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
// Wait until the page receives the cookies.
//
// Sometimes login flow sets cookies in the process of several redirects.
// Wait for the final URL to ensure that the cookies are actually set.
await page.waitForURL('https://github.com/');
// Alternatively, you can wait until the page reaches a state where all cookies are set.
await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
// End of authentication steps.
await page.context().storageState({ path: adminFile });
});
const userFile = 'playwright/.auth/user.json';
setup('authenticate as user', async ({ page }) => {
// Perform authentication steps. Replace these actions with your own.
await page.goto('https://github.com/login');
await page.getByLabel('Username or email address').fill('user');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
// Wait until the page receives the cookies.
//
// Sometimes login flow sets cookies in the process of several redirects.
// Wait for the final URL to ensure that the cookies are actually set.
await page.waitForURL('https://github.com/');
// Alternatively, you can wait until the page reaches a state where all cookies are set.
await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
// End of authentication steps.
await page.context().storageState({ path: userFile });
});
その後、設定で指定する**代わりに**、各テストファイルまたはテストグループに対してstorageState
を指定します。
import { test } from '@playwright/test';
test.use({ storageState: 'playwright/.auth/admin.json' });
test('admin test', async ({ page }) => {
// page is authenticated as admin
});
test.describe(() => {
test.use({ storageState: 'playwright/.auth/user.json' });
test('user test', async ({ page }) => {
// page is authenticated as a user
});
});
UIモードでの認証も参照してください。
複数のロールを一緒にテストする
使用するタイミング
- 単一のテストで、複数の認証済みロールがどのように連携するかをテストする必要がある場合。
詳細
同じテスト内で、異なるストレージ状態を持つ複数のBrowserContextとPageを使用します。
import { test } from '@playwright/test';
test('admin and user', async ({ browser }) => {
// adminContext and all pages inside, including adminPage, are signed in as "admin".
const adminContext = await browser.newContext({ storageState: 'playwright/.auth/admin.json' });
const adminPage = await adminContext.newPage();
// userContext and all pages inside, including userPage, are signed in as "user".
const userContext = await browser.newContext({ storageState: 'playwright/.auth/user.json' });
const userPage = await userContext.newPage();
// ... interact with both adminPage and userPage ...
await adminContext.close();
await userContext.close();
});
POMフィクスチャで複数のロールをテストする
使用するタイミング
- 単一のテストで、複数の認証済みロールがどのように連携するかをテストする必要がある場合。
詳細
各ロールとして認証されたページを提供するフィクスチャを導入できます。
以下は、2つのページオブジェクトモデル (admin POM と user POM) のためのフィクスチャを作成する例です。これは、adminStorageState.json
とuserStorageState.json
ファイルがグローバルセットアップで作成されていることを前提としています。
import { test as base, type Page, type Locator } from '@playwright/test';
// Page Object Model for the "admin" page.
// Here you can add locators and helper methods specific to the admin page.
class AdminPage {
// Page signed in as "admin".
page: Page;
// Example locator pointing to "Welcome, Admin" greeting.
greeting: Locator;
constructor(page: Page) {
this.page = page;
this.greeting = page.locator('#greeting');
}
}
// Page Object Model for the "user" page.
// Here you can add locators and helper methods specific to the user page.
class UserPage {
// Page signed in as "user".
page: Page;
// Example locator pointing to "Welcome, User" greeting.
greeting: Locator;
constructor(page: Page) {
this.page = page;
this.greeting = page.locator('#greeting');
}
}
// Declare the types of your fixtures.
type MyFixtures = {
adminPage: AdminPage;
userPage: UserPage;
};
export * from '@playwright/test';
export const test = base.extend<MyFixtures>({
adminPage: async ({ browser }, use) => {
const context = await browser.newContext({ storageState: 'playwright/.auth/admin.json' });
const adminPage = new AdminPage(await context.newPage());
await use(adminPage);
await context.close();
},
userPage: async ({ browser }, use) => {
const context = await browser.newContext({ storageState: 'playwright/.auth/user.json' });
const userPage = new UserPage(await context.newPage());
await use(userPage);
await context.close();
},
});
// Import test with our new fixtures.
import { test, expect } from '../playwright/fixtures';
// Use adminPage and userPage fixtures in the test.
test('admin and user', async ({ adminPage, userPage }) => {
// ... interact with both adminPage and userPage ...
await expect(adminPage.greeting).toHaveText('Welcome, Admin');
await expect(userPage.greeting).toHaveText('Welcome, User');
});
セッションストレージ
認証済み状態の再利用は、クッキー、ローカルストレージおよびIndexedDBベースの認証をカバーします。まれに、サインイン状態に関連する情報を保存するためにセッションストレージが使用されます。セッションストレージは特定のドメインに固有であり、ページロードを跨いで永続化されません。Playwrightはセッションストレージを永続化するAPIを提供していませんが、以下のスニペットを使用してセッションストレージを保存/読み込みすることができます。
// Get session storage and store as env variable
const sessionStorage = await page.evaluate(() => JSON.stringify(sessionStorage));
fs.writeFileSync('playwright/.auth/session.json', sessionStorage, 'utf-8');
// Set session storage in a new context
const sessionStorage = JSON.parse(fs.readFileSync('playwright/.auth/session.json', 'utf-8'));
await context.addInitScript(storage => {
if (window.location.hostname === 'example.com') {
for (const [key, value] of Object.entries(storage))
window.sessionStorage.setItem(key, value);
}
}, sessionStorage);
一部のテストで認証を回避する
テストファイルでストレージの状態をリセットすることで、プロジェクト全体に設定された認証を回避できます。
import { test } from '@playwright/test';
// Reset storage state for this file to avoid being authenticated
test.use({ storageState: { cookies: [], origins: [] } });
test('not signed in test', async ({ page }) => {
// ...
});