Playwright ページオブジェクトモデル(POM)
この章では、ページオブジェクトモデル(Page Object Model)の設計思想と実装方法を紹介し、大規模なテストスイートを整理するのに役立ちます。
POMとは
ページオブジェクトモデルは、テスト設計パターンであり、ページの要素と操作を独立したクラスにカプセル化します。
各ページオブジェクトは、Webアプリケーション内の1つのページまたはコンポーネントを表し、そのページのロケーターと操作メソッドを含みます。
POMの利点
| 利点 | 説明 |
|---|---|
| テスト作成の簡素化 | テストコードは高レベルAPI(例:loginPage.login('user', 'pass'))を使用し、生のLocator操作は使用しません |
| 保守コストの削減 | ページ構造の変更はPage Objectクラスを修正するだけでよく、各テストを修正する必要はありません |
| 可読性の向上 | テストコードがビジネス言語に近くなり、非技術者にも理解しやすくなります |
| 重複の回避 | 同じロケーターと操作は一度だけ定義すればよい |
Page Objectクラスの作成
以下ではサンプルサイトを例に、ログインページのPage Objectを作成します。
例
// ファイルパス: pages/LoginPage.ts
import { type Page, type Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
// すべてのロケーターを読み取り専用プロパティとして宣言
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
// コンストラクタ内で全てのロケーターを初期化
this.usernameInput = page.getByLabel('ユーザー名');
this.passwordInput = page.getByLabel('パスワード');
this.loginButton = page.getByRole('button', { name: 'ログイン' });
this.errorMessage = page.getByTestId('login-error');
}
// ログインページに移動
async goto() {
await this.page.goto('/login');
}
// ログイン操作を実行
async login(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
// ログイン失敗時のエラーメッセージを検証
async expectLoginError(message: string) {
await expect(this.errorMessage).toBeVisible();
await expect(this.errorMessage).toContainText(message);
}
}
import { type Page, type Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
// すべてのロケーターを読み取り専用プロパティとして宣言
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
// コンストラクタ内で全てのロケーターを初期化
this.usernameInput = page.getByLabel('ユーザー名');
this.passwordInput = page.getByLabel('パスワード');
this.loginButton = page.getByRole('button', { name: 'ログイン' });
this.errorMessage = page.getByTestId('login-error');
}
// ログインページに移動
async goto() {
await this.page.goto('/login');
}
// ログイン操作を実行
async login(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
// ログイン失敗時のエラーメッセージを検証
async expectLoginError(message: string) {
await expect(this.errorMessage).toBeVisible();
await expect(this.errorMessage).toContainText(message);
}
}
テストでPage Objectを使用する
例
// ファイルパス: tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test.describe('ログイン機能', () => {
test('正しいユーザー名とパスワードでログイン成功', async ({ page }) => {
const loginPage = new LoginPage(page);
// Page Objectのメソッドを使用
await loginPage.goto();
await loginPage.login('サンプル_user', 'correct_password');
// ログイン成功後にリダイレクトを検証
await expect(page).toHaveURL('/dashboard');
});
test('誤ったパスワードでログイン失敗', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('サンプル_user', 'wrong_password');
// Page Objectのアサーションメソッドを使用
await loginPage.expectLoginError('ユーザー名またはパスワードが正しくありません');
});
});
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test.describe('ログイン機能', () => {
test('正しいユーザー名とパスワードでログイン成功', async ({ page }) => {
const loginPage = new LoginPage(page);
// Page Objectのメソッドを使用
await loginPage.goto();
await loginPage.login('サンプル_user', 'correct_password');
// ログイン成功後にリダイレクトを検証
await expect(page).toHaveURL('/dashboard');
});
test('誤ったパスワードでログイン失敗', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('サンプル_user', 'wrong_password');
// Page Objectのアサーションメソッドを使用
await loginPage.expectLoginError('ユーザー名またはパスワードが正しくありません');
});
});
複数層のページオブジェクト
実際のプロジェクトでは、複数のPage Objectを作成でき、それらは相互に関連付けることができます。
例
// ファイルパス: pages/DashboardPage.ts
import { type Page, type Locator, expect } from '@playwright/test';
export class DashboardPage {
readonly page: Page;
readonly welcomeText: Locator;
readonly logoutButton: Locator;
constructor(page: Page) {
this.page = page;
this.welcomeText = page.getByText('おかえりなさい');
this.logoutButton = page.getByRole('button', { name: 'ログアウト' });
}
async expectWelcomeMessage(username: string) {
await expect(this.welcomeText).toContainText(username);
}
async logout() {
await this.logoutButton.click();
}
}
import { type Page, type Locator, expect } from '@playwright/test';
export class DashboardPage {
readonly page: Page;
readonly welcomeText: Locator;
readonly logoutButton: Locator;
constructor(page: Page) {
this.page = page;
this.welcomeText = page.getByText('おかえりなさい');
this.logoutButton = page.getByRole('button', { name: 'ログアウト' });
}
async expectWelcomeMessage(username: string) {
await expect(this.welcomeText).toContainText(username);
}
async logout() {
await this.logoutButton.click();
}
}
ページオブジェクトの組み合わせ使用:
例
test('ログインとログアウトの完全なフロー', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
// ログイン
await loginPage.goto();
await loginPage.login('サンプル_user', 'password');
// ダッシュボードを検証
await dashboardPage.expectWelcomeMessage('サンプル_user');
// ログアウト
await dashboardPage.logout();
await expect(page).toHaveURL('/login');
});
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
// ログイン
await loginPage.goto();
await loginPage.login('サンプル_user', 'password');
// ダッシュボードを検証
await dashboardPage.expectWelcomeMessage('サンプル_user');
// ログアウト
await dashboardPage.logout();
await expect(page).toHaveURL('/login');
});
POMのベストプラクティス
| 実践 | 説明 |
|---|---|
| Locatorをコンストラクタに集中 | すべてのロケーターはコンストラクタで定義し、メソッド内で動的に作成しない |
| メソッドはPage Objectを返す | 操作メソッドは新しいPage Objectを返すことができます(例:ログイン後にDashboardPageを返す) |
| アサーションをカプセル化するが、乱用しない | よく使われるアサーションの組み合わせはメソッドにカプセル化し、単純なアサーションはテストに直接書きます |
| 継承ではなくコンポジションを使用 | 深い継承を避け、コンポジションパターンで複数のPage Objectを関連付けます |
| 1セットのオブジェクトで1つのページ/コンポーネントをカバー | 粒度は適度にし、小さな要素ごとに個別のクラスを作成しないでください |
その他の拡張POMは必須ではありません。
テストが数個しかない小規模プロジェクトでは、テストに直接Locatorを書く方が明確です。テストファイルが5つ以上になったら、POMの導入を検討してください。