はじめに
こんにちは。某自社開発企業でSET(Software Engineer in Test)として働いている、たくまです。今回は、フロントエンドの自動テストにおいて Playwright を活用した実装方法をご紹介します。
現在、業務でPlaywrightを用いたE2Eテスト自動化に取り組んでおり、以下の観点を中心に解説します:
- どのような観点で設計・実装しているか
- 実装時に意識しているポイントは何か
- 保守性・可読性を重視した設計パターン
これからPlaywrightでテストを書いてみたい方の参考になれば幸いです。
- 自動テストなどの定義など、実装方法は企業毎に違います。この記事は強制するものではありません。
目次
ディレクトリ構成
基本方針
E2Eテストの設計で最も重要なのは、画面構成の把握とPage Object Model(POM)の責務分離です。
UIの仕様変更に柔軟に対応できるよう、画面ごとにディレクトリを分ける構成を採用しています。
画面単位でのディレクトリ分割
- ログイン画面 →
login/
ディレクトリ - ダッシュボード画面 →
dashboard/
ディレクトリ - 設定画面 →
settings/
ディレクトリ
機能ごとのファイル分割によるメリット
各画面ディレクトリ内では、機能ごとにファイルを分割することで以下のメリットを得られます:
- 修正時の影響範囲を限定(保守性向上)
- Spec側からの可読性が高い
- 将来的な仕様変更に柔軟に対応
失敗からの学び
過去にcommon/
やbasePage.ts
に共通処理を詰め込みすぎて、ファイル肥大化や責務混在を引き起こした経験があります。
この反省を活かし、1機能1ファイルの原則を意識した構造にしています。
ディレクトリ構成例
e2e_test/
├── page/ # Page Object Model用ディレクトリ
│ ├── login/
│ │ └── LoginPage.ts # ログイン画面用POM
│ └── dashboard/
│ └── DashboardPage.ts # ダッシュボード画面用POM
└── spec/ # テストスクリプト用ディレクトリ
└── login/
└── login.spec.ts # ログイン〜ダッシュボード表示確認テスト
POM(Page Object Model)の実装
POMとは
Page Object Model(POM) は、画面ごとの操作を抽象化してクラス化する設計パターンです。
Spec側では、POMで定義された操作メソッドを呼び出すだけでテストを記述でき、以下の効果が得られます:
- 保守性の向上 - UI変更時の修正箇所を局所化
- 可読性の向上 - 自然言語に近い記述
- 再利用性の向上 - 複数のテストから同じ操作を利用
実装方針
項目 | 方針 | 理由 |
---|---|---|
ファイル構成 | 1ファイル = 1画面 or 1責務 | 責務の明確化 |
コンストラクタ | Locatorの初期化のみ | シンプルで予測可能 |
メソッド設計 | 操作に関して単一責務 | 理解しやすさ重視 |
検証処理 |
expect はSpec側で行う |
関心の分離 |
セレクタ優先度 | アクセシビリティ優先 | 保守性・安定性 |
コメント | 日本語で意図を明記 | チーム開発での理解促進 |
セレクタの優先順位
-
getByRole()
- セマンティックな要素の取得 -
getByLabel()
- ラベルに基づく取得 -
getByTestId()
- テスト専用ID -
locator()
- 最後の手段(理由をコメントで明記)
実装例
LoginPage.ts
// e2e_test/page/login/LoginPage.ts
import { type Locator, type Page } from '@playwright/test'
/**
* ログイン画面の操作を管理するPage Object Model
*/
export class LoginPage {
readonly page: Page
readonly emailInput: Locator
readonly passwordInput: Locator
readonly submitButton: Locator
readonly errorMessage: Locator
readonly loginFormContainer: Locator
constructor(page: Page) {
this.page = page
// アクセシビリティを優先したセレクタを使用
this.emailInput = page.getByLabel('メールアドレス')
this.passwordInput = page.getByLabel('パスワード')
this.submitButton = page.getByRole('button', { name: 'ログイン' })
this.errorMessage = page.getByTestId('login-error-message')
// CSSセレクタを使用する場合は理由を明記
// 理由: アクセシビリティ対応が未完了のため、将来的にgetByXXXで取得予定
this.loginFormContainer = page.locator('.login-form-wrapper')
}
/**
* ログイン画面に遷移する
*/
async gotoLoginPage(): Promise<void> {
await this.page.goto('/login')
}
/**
* メールアドレスを入力する
* @param email 入力するメールアドレス
*/
async enterEmail(email: string): Promise<void> {
await this.emailInput.fill(email)
}
/**
* パスワードを入力する
* @param password 入力するパスワード
*/
async enterPassword(password: string): Promise<void> {
await this.passwordInput.fill(password)
}
/**
* ログインボタンをクリックする
*/
async clickLoginButton(): Promise<void> {
await this.submitButton.click()
}
}
DashboardPage.ts
// e2e_test/page/dashboard/DashboardPage.ts
import { type Locator, type Page } from '@playwright/test'
/**
* ダッシュボード画面の操作を管理するPage Object Model
*/
export class DashboardPage {
readonly page: Page
readonly dashboardTitle: Locator
constructor(page: Page) {
this.page = page
// セマンティックなheading要素を優先的に取得
this.dashboardTitle = page.getByRole('heading', { name: 'ダッシュボード' })
}
/**
* ダッシュボード画面が表示されるまで待機する
*/
async waitForVisible(): Promise<void> {
await this.dashboardTitle.waitFor()
}
}
Spec(テストスクリプト)の実装
Specの役割
Spec(テストスクリプト) は、具体的なE2Eテストケースの振る舞いを記述するファイルです。
POMで抽象化された操作を組み合わせ、最終的な期待値を expect
で検証することで、テストケースとしての完結性を保ちます。
実装方針
テスト構造の3原則
前提条件 → 操作 → 検証
↓ ↓ ↓
Given When Then
フェーズ | 内容 | 実装例 |
---|---|---|
前提条件 | テスト実行前の状態設定 |
beforeEach での初期化 |
操作 | POMを通じたUI操作 | loginPage.enterEmail() |
検証 | 期待値との比較 | expect().toBeVisible() |
設計指針
- 責務分離 - POMにUI操作を委譲、Specはテストの意図と流れに集中
-
グルーピング -
test.describe()
で機能単位にまとめる -
共通化 -
beforeEach
・afterAll
で前後処理を統一 - 可読性 - 日本語コメントで非エンジニアにも理解しやすく
Playwrightのexpect()
の特長
Playwrightの expect
は、検証対象が期待された状態になるまで最大5秒間(デフォルト)自動リトライします。
これにより、非同期UIにも強く、テストの安定性が飛躍的に向上します。
expect
活用のポイント
- 適切なマッチャー選択 - 検証内容に応じた最適なアサーション
-
自動リトライ活用 - 明示的な
waitFor()
の多用を避ける - 意図の明確化 - マッチャー名で検証内容を自己文書化
実装例
// ログイン成功後、ダッシュボード画面が表示されることを検証する
await expect(dashboardPage.dashboardTitle).toBeVisible()
よく使うアサーション一覧
検証内容 | アサーション | 使用場面 |
---|---|---|
要素の表示状態 | toBeVisible() |
画面遷移の確認 |
テキスト内容 | toHaveText() |
メッセージ表示の検証 |
入力値 | toHaveValue() |
フォーム入力の確認 |
要素の属性 | toHaveAttribute() |
CSS class、href等の検証 |
要素数 | toHaveCount() |
リスト項目数の確認 |
要素の非表示 | toBeHidden() |
モーダル閉じ等の確認 |
テストスクリプト実装例
// e2e_test/spec/login/login.spec.ts
/**
* ログイン機能のE2Eテスト
* 概要: ログイン成功後にダッシュボード画面が表示されることを確認
*/
import { test, expect } from '@playwright/test'
import { LoginPage } from '../../page/login/LoginPage'
import { DashboardPage } from '../../page/dashboard/DashboardPage'
import { execSync } from 'node:child_process'
test.describe('ログイン機能', () => {
test.beforeEach(async ({ page }) => {
// テスト前処理:Dockerコンテナ内でのセットアップ(DBリセットや初期状態)
execSync('docker exec my-app-container npm run test:setup', { stdio: 'inherit' })
// ログインのインスタンスを作成する
const loginPage = new LoginPage(page)
await loginPage.gotoLoginPage()
})
test.afterAll(async () => {
// テスト後処理:Docker環境のクリーンアップやログ収集など
execSync('docker exec my-app-container npm run test:teardown', { stdio: 'inherit' })
})
test('ユーザーがログインできることを検証する', async ({ page }) => {
// インスタンスを作成する
const loginPage = new LoginPage(page)
// メールアドレスを入力する
await loginPage.enterEmail('test@example.com')
// パスワードを入力する
await loginPage.enterPassword('validPassword123')
// 「ログイン」ボタンをクリックする
await loginPage.clickLoginButton()
// インスタンスを作成する
const dashboardPage = new DashboardPage(page)
// ログイン後の画面(例: /dashboard)が表示されていることを検証する
await expect(dashboardPage.dashboardTitle).toBeVisible()
})
})
Playwright E2Eテスト実装チェックリスト
このチェックリストは、POM + Specパターンでの実装品質を担保し、AIレビューの基準としても活用できます。
## ディレクトリ構成
- [ ] `e2e_test/page/` に画面単位でディレクトリが分割されている
- 例: `login/`, `dashboard/`, `settings/`
- [ ] 各ディレクトリ内は **1機能 = 1ファイル** でPOMクラスを定義している
- [ ] `spec/` 配下は画面・機能単位で整理され、対応するPOMと関連している
## POM(Page Object Model)
### 基本構造
- [ ] クラス名が `{機能名}Page` 形式で統一されている
- 例: `LoginPage`, `DashboardPage`
- [ ] コンストラクタ内で **Locatorの初期化のみ** を行っている
- [ ] UI操作メソッドの命名が明確で一貫している
- 例: `clickLoginButton()`, `enterPassword()`
### 責務分離
- [ ] **検証(expect)は含まれていない**
- [ ] 各メソッドが単一責務を持っている
- [ ] Specとの役割分担が明確になっている
### セレクタ戦略
- [ ] 以下の優先順位でLocatorを取得している:
1. [ ] `getByRole()` - セマンティックな要素
2. [ ] `getByLabel()` - ラベルベース
3. [ ] `getByTestId()` - テスト専用ID
4. [ ] `locator()` - 最後の手段(理由を明記)
### ドキュメンテーション
- [ ] 日本語コメントでUI制約や意図が説明されている
- [ ] `locator()`使用時は理由がコメントで明記されている
- [ ] TSDocでメソッドの説明が記載されている
## 📝 Spec(テストスクリプト)
### テスト構造
- [ ] テストが「**前提 → 操作 → 検証**」の順で整理されている
- [ ] UI操作はすべてPOMを経由して行われている
- [ ] `test.describe()`で機能単位にグルーピングされている
### 前後処理
- [ ] `beforeEach`で初期化処理が共通化されている
- [ ] `afterAll`で後始末処理が記述されている
- [ ] テストデータの準備・クリーンアップが適切
### 検証処理
- [ ] `expect()`はSpec側にのみ記述されている
- [ ] テストケースの意図が明確に表現されている
## アサーション(expect)
### マッチャー選択
- [ ] 意図に合ったマッチャーが使われている
- 表示確認 → `toBeVisible()`
- テキスト確認 → `toHaveText()`
- 数量確認 → `toHaveCount()`
- [ ] 自動リトライを前提に、`waitFor()`の乱用を避けている
- [ ] 複数要素の検証で適切なマッチャーを使用している
### 安定性
- [ ] DOM変更タイミングが不安定な箇所に明確な検証意図が記述されている
- [ ] フレーク(不安定)要素への対策が実装されている
## 🤖 AI活用・レビュー観点
### コード品質
- [ ] 本チェックリストをもとにAIレビューを実施した
- [ ] `locator()`使用理由が自然言語で明記されている
- [ ] 操作名・変数名が **自然言語として理解できるレベル**
### 設計品質
- [ ] 同一画面内の複数責務が適切にファイル分離されている
- [ ] 重複処理を適切に共通化し、**可読性を重視**している
- [ ] POMとSpecの責務分離が明確
## 📋 補足項目(推奨)
### ネーミング規約
- [ ] ファイル名とクラス名が一致している
- 例: `LoginPage.ts` ⇔ `class LoginPage`
- [ ] テストケース名が実行内容を表現している
### 運用面
- [ ] テスト実行時のログ出力が必要最小限
- [ ] デバッグ用の情報が適切に出力される
- [ ] CI/CD環境での実行を考慮した設計
### 拡張性
- [ ] Allure、Trace Viewer等のレポートツール対応
- [ ] 並列実行を考慮したテスト設計
- [ ] 環境依存要素の適切な抽象化
## 品質基準
このチェックリストの **80%以上** を満たすことを品質基準とし、継続的な改善を行ってみてください〜!
今回はこちらで終わりです!ありがとうございました!