Playwrightで毎回同じログイン操作を書いているなら、setup projectで1回ログインし、storageStateを各テストへ読み込ませる形にできます。テストごとのブラウザ環境は分けたまま、認証済みの状態から始められます。
この記事では、auth.setup.ts、projectの依存設定、ログインを省いたテストの3ファイルを作ります。対象は、同じアカウントで並行しても互いに影響しない、読み取り中心のテストです。実行確認はPlaywright 1.61.1で行いました。
共有するのは、ページではなく保存した認証状態
- setup用のブラウザでログインする。
- ログイン完了を確認し、状態をJSONへ保存する。
- 各テストが新しいbrowser contextを作り、そのJSONを読み込む。
一つのページを全テストで操作し続ける方法ではありません。今回の検証でも、片方のcontextでlocalStorageを書き換えても、もう片方の値は変わりませんでした。
ただし、ブラウザが別でもサーバー側では同じアカウントです。プロフィールの更新、共有カートの削除、全セッションのログアウトなどを同時に行うと、ほかのテストへ影響することがあります。そうしたテストは、後述するアカウントの分離を先に考えます。
以下のコードは、テスト用アプリが起動済みで、次の画面を持つ前提です。URL・ラベル・ログイン後の見出しは自分のアプリへ置き換えてください。
http://127.0.0.1:5201/loginにEmail・Password欄とSign inボタンがある。- ログイン後は
/dashboardへ移り、My accountという見出しが出る。 - アカウント画面からAccount detailsへ移動できる。
ローカル検証では、この画面を持つ専用の小さなサーバーを使いました。実サービスのアカウントではなく、テスト用の認証状態で確認しています。
auth.setup.tsで、ログイン完了を待ってから保存する
tests/auth.setup.tsを作成します。認証情報は環境変数から受け取り、最後のURLとアカウント画面を確認してから保存します。
import { test as setup, expect } from '@playwright/test';
import path from 'node:path';
const authFile = path.join(__dirname, '../playwright/.auth/user.json');
setup('ログインして認証状態を保存する', async ({ page }) => {
const email = process.env.E2E_EMAIL;
const password = process.env.E2E_PASSWORD;
if (!email || !password) {
throw new Error('E2E_EMAIL と E2E_PASSWORD を設定してください');
}
await page.goto('/login');
await page.getByLabel('Email', { exact: true }).fill(email);
await page.getByLabel('Password', { exact: true }).fill(password);
await page.getByRole('button', { name: 'Sign in', exact: true }).click();
await page.waitForURL('**/dashboard');
await expect(page.getByRole('heading', { name: 'My account', exact: true })).toBeVisible();
await page.context().storageState({ path: authFile });
});
ボタンをクリックした直後では、途中のリダイレクトやCookieの設定が終わっていないことがあります。保存の位置は「操作を送った後」ではなく、アプリ側のログインが完了したと確認できる位置にします。アプリによっては、ユーザー情報の読み込み完了まで待つ必要もあります。
保存先は、setupファイルから見た../playwright/.auth/user.jsonです。設定ファイルから指定するパスも、同じファイルへ揃えます。
このJSONにはログイン状態を再利用できるCookieなどが入ります。作る前に、プロジェクトの.gitignoreへ次を追加します。CIの成果物にも含めません。
playwright/.auth/
.env
認証情報がないときは、空文字でログインを試さず、環境変数が必要だというエラーで止めています。.envを置くだけで、このコードが自動的に読むわけではありません。普段使っている環境変数の読み込み方法、またはCIのシークレット設定から渡してください。
projectsのdependenciesで、保存を先に終わらせる
playwright.config.tsで、setupをchromiumの依存先にします。setup側は空の状態でログインし、chromium側だけが保存済みの状態を読み込みます。
import { defineConfig, devices } from '@playwright/test';
import path from 'node:path';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
workers: 2,
use: {
baseURL: 'http://127.0.0.1:5201',
channel: process.env.PW_CHANNEL || undefined,
trace: 'off',
},
projects: [
{
name: 'setup',
testMatch: /auth\.setup\.ts/,
use: { storageState: { cookies: [], origins: [] } },
},
{
name: 'chromium',
testMatch: /.*\.spec\.ts/,
dependencies: ['setup'],
use: {
...devices['Desktop Chrome'],
storageState: path.join(__dirname, 'playwright/.auth/user.json'),
},
},
],
});
dependencies: ['setup']があるため、setup成功後にchromiumのテストが始まります。testMatchも分け、setupファイルが通常のテストとして重複実行されないようにしています。
この構成では、通常のCLI実行ごとにsetupが認証状態を作り直します。「JSONが存在すればログインを省く」という条件は入れていません。以前のファイルが残っていても、今回のログインが失敗したら後続テストを実行しない構成です。
PW_CHANNELを指定しなければPlaywrightのChromiumを使います。ローカルでインストール済みのChromeを使いたい場合だけ、PW_CHANNEL=chromeを設定できます。複数ブラウザへの展開は、認証方式が同じ状態を共有できるか確認してから行います。
この例は起動済みのアプリへ接続するため、開発サーバーの起動処理を含めていません。既存のwebServer設定がある場合は残します。サーバー起動やHookとの連携を含む全体像は、PlaywrightでWebアプリの変更を自動確認する記事で説明しています。
通常のテストからログイン操作を外す
tests/account.spec.tsでは、最初からアカウント画面へ移動します。認証できているつもりで先へ進まず、ログイン後にだけ出る見出しや要素を確認します。
import { test, expect } from '@playwright/test';
test('アカウント画面を開ける', async ({ page }) => {
await page.goto('/dashboard');
await expect(page.getByRole('heading', { name: 'My account', exact: true })).toBeVisible();
await expect(page.getByText('Reader account', { exact: true })).toBeVisible();
});
test('別テストからもログイン済みで開ける', async ({ page }) => {
await page.goto('/dashboard');
await expect(page.getByRole('link', { name: 'Account details', exact: true })).toBeVisible();
await page.getByRole('link', { name: 'Account details', exact: true }).click();
await expect(page.getByRole('heading', { name: 'Account details', exact: true })).toBeVisible();
});
プロジェクトのルートで、認証情報を設定したうえで実行します。
npx playwright test --project=chromium
この指定でも依存先のsetupが先に実行されます。--no-depsを付けると依存先を飛ばすため、今回の「毎回作り直す」前提では付けません。実行順の詳細はPlaywrightのproject依存関係を参照できます。
ローカル検証では、認証1回のあと、上の2テストと認証状態を調べる4テストが通りました。ログイン回数をサーバー側で数え、後続テストが再ログインしていないことも確認しています。
Cookie・localStorage・sessionStorageを混同しない
保存状態を読み込んでもログイン画面へ戻る場合は、アプリがどこに認証情報を持っているかを確認します。
| 保存先 | 今回のstorageStateでの扱い |
|---|---|
| Cookie | 保存・読み込みの対象。検証では認証Cookieを引き継げた |
| localStorage | 対象originの値を保存・読み込み。検証では表示設定も引き継げた |
| sessionStorage | この方法では保存されない。検証でも別contextへは引き継がれなかった |
| IndexedDB | 必要なら保存時にindexedDB: trueを指定する。今回の実行例では未使用 |
IndexedDBに認証情報を持つアプリでは、保存の呼び出しをpage.context().storageState({ path: authFile, indexedDB: true })に変えることを検討します。このオプションはPlaywright 1.51以降です。保存対象の詳細はstorageStateのAPIリファレンスにあります。
sessionStorageが認証に必要なら、JSONのファイル名を変えるだけでは解決しません。アプリに合う初期化方法やテスト用の認証手順を別途用意します。また、localhostと127.0.0.1は同じoriginではありません。保存時とテスト時でURLを混ぜていないかも確認してください。
期限切れ・複数ロール・CIでは、どこを変えるか
| 困っていること | 見直す場所 |
|---|---|
| JSONはあるのにログインできない | Cookieの期限、サーバー側のセッション失効、接続先、保存のタイミング |
| UI modeで古い認証状態を使ってしまう | setupを選んで明示的に再実行する。通常のCLIと同じ自動実行を想定しない |
| 管理者と一般ユーザーを試したい | 認証アカウント・保存ファイル・利用するprojectまたはfixtureをロール別に分ける |
| 並列テストで設定やデータが壊れる | サーバー側で共有しているデータを確認し、workerごとのアカウントなどへ分ける |
| CIだけログインに失敗する | 認証情報の注入、テスト環境のURL、アカウント状態、追加認証の有無 |
今回の検証では、保存状態のCookieを期限切れにすると、JSONが残っていてもログイン画面へ戻りました。また、誤ったパスワードでsetupを実行すると、後続の6テストは実行されませんでした。「ファイルがあること」と「認証が有効なこと」は別です。
共有アカウントの適用範囲やworkerごとのアカウント、UI modeの認証については、公式のAuthenticationガイドに説明があります。MFAや外部IdPが必要なアプリでは、保存ファイルだけでその手順を不要にできるとは限りません。テスト環境で利用できる認証方法を確認します。
CIではテスト専用アカウントの情報を環境変数へ渡し、実行中に認証状態を作ります。認証JSONを長期キャッシュしたり、失敗レポートと一緒に公開したりしないよう、保存先を分けておきます。ログイン中のtraceや画面にも認証情報が入り得るため、この最小例ではtraceをoffにしています。
まずは読み取り専用の2テストで、setupが1回、そのあとに各テストが動くことを確認しましょう。ログインの重複を減らしながら、期限切れや認証失敗を正しく検出できる形にできます。