はじめに
E2Eテスト、書いてますか。
正直なところ、E2Eテストって面倒なイメージがありますよね。Seleniumで何度も痛い目を見てきた身としては、「テストが不安定すぎて信用できない」「セットアップが面倒」という話、よくわかります。
そんな中で最近注目を集めているのがPlaywrightです。Microsoft製のE2Eテストフレームワークで、GitHubのスター数は約8万、月間ダウンロード数は1億回を超えています。これ、意外と知らない人も多いんですが、VS CodeやBing、Outlookなど、Microsoft内部でも実際に使われているそうです。
個人的には、Playwrightを使い始めてからE2Eテストへの苦手意識がかなり薄れました。今回はその理由を含めて、Playwrightの魅力を紹介していきます。
Playwrightとは
Playwrightは、Webアプリケーションのテストと自動化のためのフレームワークです。2020年にMicrosoftがリリースして以来、急速にシェアを伸ばしています。
主な特徴は以下の通りです。
- クロスブラウザ対応: Chromium、Firefox、WebKitを単一のAPIでテスト可能
- マルチ言語対応: TypeScript、JavaScript、Python、.NET、Javaに対応
- 自動待機機能: 要素が操作可能になるまで自動で待機
- 強力な開発ツール: コード生成、インスペクター、トレースビューアー
特にSeleniumからの乗り換え組として嬉しいのは、ブラウザドライバーの管理が不要という点ですね。npm install一発で必要なブラウザも一緒にインストールされます。
特徴・メリット
1. 自動待機機能で不安定なテストとおさらば
Playwrightの最大の特徴は「自動待機(Auto-wait)」機能です。
従来のテストフレームワークでは、要素が表示されるまで明示的にsleepやwaitを入れる必要がありました。これが原因で「ローカルでは通るけどCIで落ちる」という不安定なテストが量産されていたわけです。
Playwrightでは、クリックやテキスト入力といったアクションを実行する前に、自動で要素の状態をチェックしてくれます。要素が表示されていない、クリック可能でない、といった場合は自動でリトライしてくれる。これだけでテストの安定性が劇的に向上します。
2. Web-first Assertionsが便利
テストのアサーション(検証)も賢くなっています。
await expect(page.getByRole('button')).toBeEnabled();
このようなアサーションは、条件が満たされるまで自動でリトライしてくれます。SPAのような動的なWebアプリでも、いちいち待機処理を書く必要がありません。
3. コード生成機能(Codegen)
これが個人的に一番推したい機能です。
npx playwright codegen https://example.com
このコマンドを実行すると、ブラウザが起動して、実際の操作を記録してテストコードを自動生成してくれます。マウス操作やキーボード入力がそのままコードになる。テストを書く時間が大幅に短縮できます。
もちろん生成されたコードをそのまま使うのではなく、適宜リファクタリングは必要ですが、ゼロから書くよりは圧倒的に楽ですね。
4. Trace Viewerでデバッグが楽
テストが失敗したとき、何が起きたのかを調査するのは面倒です。Playwrightには「Trace Viewer」という機能があり、テスト実行時のスクリーンショット、DOMスナップショット、ネットワークログなどを記録できます。
失敗したテストの実行状況を後から詳細に確認できるので、「なんで落ちたんだ?」というストレスが軽減されます。
インストール方法
Node.jsがインストールされている環境であれば、すぐに始められます。
新規プロジェクトの場合
npm init playwright@latest
対話形式でセットアップが進みます。TypeScriptかJavaScriptか、テストの配置場所、GitHub Actionsの設定ファイルを生成するかなどを選択できます。
既存プロジェクトに追加する場合
npm install -D @playwright/test
npx playwright install
2つ目のコマンドで、テストに必要なブラウザ(Chromium、Firefox、WebKit)がインストールされます。
基本的な使い方
テストファイルの作成
testsディレクトリにテストファイルを作成します。
// tests/example.spec.ts
import { test, expect } from '@playwright/test';
test('ホームページのタイトルを確認', async ({ page }) => {
await page.goto('https://example.com');
// タイトルの確認
await expect(page).toHaveTitle(/Example/);
});
test('リンクをクリックして遷移', async ({ page }) => {
await page.goto('https://example.com');
// リンクをクリック
await page.getByRole('link', { name: 'More information' }).click();
// 遷移後のURLを確認
await expect(page).toHaveURL(/iana\.org/);
});
テストの実行
# 全テストを実行
npx playwright test
# 特定のファイルを実行
npx playwright test tests/example.spec.ts
# UIモードで実行(デバッグに便利)
npx playwright test --ui
# 特定のブラウザで実行
npx playwright test --project=chromium
ロケーターの使い方
Playwrightでは、要素の取得に「ロケーター」を使います。
// ロール(役割)で取得 - 推奨
page.getByRole('button', { name: '送信' })
page.getByRole('textbox', { name: 'メールアドレス' })
// テキストで取得
page.getByText('ログイン')
// ラベルで取得(フォーム要素に便利)
page.getByLabel('パスワード')
// テスト用属性で取得
page.getByTestId('submit-button')
// CSSセレクタ(最終手段)
page.locator('.submit-btn')
getByRoleを使うとアクセシビリティ的にも正しいマークアップになっているかを間接的にテストできるので、個人的にはこれを優先的に使っています。
実践的なユースケース
ログインフローのテスト
import { test, expect } from '@playwright/test';
test('ログインが正常に完了する', async ({ page }) => {
await page.goto('/login');
// フォーム入力
await page.getByLabel('メールアドレス').fill('test@example.com');
await page.getByLabel('パスワード').fill('password123');
// ログインボタンをクリック
await page.getByRole('button', { name: 'ログイン' }).click();
// ダッシュボードに遷移することを確認
await expect(page).toHaveURL('/dashboard');
await expect(page.getByText('ようこそ')).toBeVisible();
});
認証状態の再利用
毎回ログイン処理を実行するのは時間の無駄です。Playwrightでは認証状態を保存して再利用できます。
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
projects: [
// 認証状態を作成
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
setup('ログイン', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('メールアドレス').fill('test@example.com');
await page.getByLabel('パスワード').fill('password123');
await page.getByRole('button', { name: 'ログイン' }).click();
await page.waitForURL('/dashboard');
await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
これで各テストはログイン済みの状態から始まります。テスト実行時間の短縮になりますね。
APIレスポンスのモック
外部APIに依存するテストは不安定になりがちです。Playwrightではネットワークリクエストをインターセプトしてモックできます。
test('APIエラー時のエラーメッセージ表示', async ({ page }) => {
// APIレスポンスをモック
await page.route('**/api/users', async route => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal Server Error' }),
});
});
await page.goto('/users');
// エラーメッセージが表示されることを確認
await expect(page.getByText('データの取得に失敗しました')).toBeVisible();
});
まとめ
Playwrightを使ってみて感じたのは、E2Eテストの「面倒くさい」部分がかなり解消されているということです。
- 自動待機機能でテストが安定する
- コード生成で初期コストが下がる
- Trace Viewerでデバッグが楽になる
- 認証状態の再利用でテストが速くなる
正直なところ、一度Playwrightを使うとSeleniumには戻れないですね。これからE2Eテストを始める人も、Seleniumから乗り換えを検討している人も、一度試してみる価値はあると思います。
公式ドキュメントも充実しているので、まずはnpm init playwright@latestで触ってみてください。