T-CREATOR

Playwright の Selectors 活用で壊れにくいテストを書く

Playwright の Selectors 活用で壊れにくいテストを書く

Web アプリケーションの品質を担保するために、E2E テストは欠かせない存在となっています。しかし、多くの開発チームが「テストが頻繁に壊れる」「保守に時間がかかりすぎる」といった課題を抱えているのではないでしょうか。

Playwright を使ったテストにおいて、この問題の根本的な原因の一つが Selectors(セレクター)の選択方法 にあります。適切なセレクター戦略を採用することで、DOM の変更に強く、保守しやすいテストコードを実現できるでしょう。

本記事では、Playwright の Selectors を活用して壊れにくいテストを書く方法を、基礎から応用まで段階的に解説いたします。初心者の方にもわかりやすく、実際の開発現場ですぐに活用できる内容をお届けします。

背景

E2E テストが壊れやすい理由

E2E テストが壊れやすい主な理由として、以下の要因が挙げられます。

DOM 構造の頻繁な変更により、CSS クラス名や要素の階層が予期せず変わってしまうことがあります。デザインのリニューアルや機能追加によって、テストで使用していたセレクターが無効になってしまうケースは珍しくありません。

動的コンテンツの存在も大きな要因です。ユーザーの操作やデータの状態によって表示される内容が変わる現代の Web アプリケーションでは、固定的なセレクターでは対応しきれない場面が多々あります。

mermaidflowchart TD
    A[E2Eテスト実行] --> B{DOM構造確認}
    B -->|変更なし| C[テスト成功]
    B -->|変更あり| D[セレクター取得失敗]
    D --> E[テスト失敗]
    E --> F[デバッグ・修正作業]
    F --> G[保守コスト増大]

図の補足: DOM 構造の変更がテスト失敗の連鎖を引き起こし、最終的に保守コストの増大につながる流れを示しています。

従来の要素選択手法の限界

従来多く用いられてきた CSS セレクターや XPath には、それぞれ課題があります。

CSS セレクターは記述が簡潔で理解しやすい一方、スタイリングの変更に影響されやすいという弱点があります。.btn-primary のようなクラス名は、デザインシステムの更新時に容易に変更される可能性が高いでしょう。

XPath は強力な選択能力を持ちますが、記述が複雑になりがちで、可読性や保守性に課題があります。特に DOM の階層構造が変わった場合、XPath 全体を見直す必要が生じることもあります。

Playwright Selectors の特徴

Playwright は、これらの課題を解決するために設計された革新的なセレクター機能を提供しています。

セマンティック重視のアプローチ を採用しており、要素の見た目や技術的な実装詳細よりも、その要素が持つ意味や役割に注目してセレクターを構築できます。

複数の選択手法を組み合わせる柔軟性 も大きな特徴です。一つの手法に依存せず、状況に応じて最適な選択方法を使い分けることが可能です。

自動待機機能 により、動的コンテンツの読み込み完了を適切に待つことができ、タイミング由来のテスト失敗を大幅に削減できます。

課題

要素の特定方法による保守性の違い

テストの保守性は、どのような方法で要素を特定するかによって大きく変わります。

脆弱な選択方法として、CSS クラス名に依存したセレクターが挙げられます。以下のような選択方法は、スタイルの変更時に簡単に壊れてしまいます。

typescript// 脆弱な例:CSS クラスに依存
await page.click('.btn-submit-form');
await page.fill('.input-user-email', 'test@example.com');

一方、要素の意味や役割に基づいた選択方法は、デザインの変更に影響されにくい特徴があります。

typescript// 堅牢な例:要素の役割に基づく選択
await page.click('button[type="submit"]');
await page.fill('input[name="email"]', 'test@example.com');

このような違いが、長期的なテスト保守コストに大きな影響を与えることになります。

DOM 変更に強い選択戦略の必要性

現代の Web 開発では、DOM 構造が頻繁に変更されることが一般的です。

フロントエンドフレームワークの活用により、コンポーネント単位での開発が主流となっています。React、Vue.js、Angular などでは、コンポーネントの内部構造が実装の都合で変更されることがよくあります。

レスポンシブデザインの実装により、画面サイズに応じて DOM 構造が動的に変化することも増えています。

mermaidsequenceDiagram
    participant Dev as 開発者
    participant DOM as DOM構造  
    participant Test as E2Eテスト
    
    Dev->>DOM: 機能追加・デザイン変更
    DOM->>Test: セレクター対象要素の変化
    Test->>Test: テスト実行
    Note over Test: セレクター取得失敗
    Test->>Dev: テスト失敗通知
    Dev->>Test: セレクター修正

この図から分かるように、DOM 変更に強い選択戦略を持たないと、開発サイクルが滞ってしまう可能性があります。

チーム開発での一貫性確保

複数の開発者が関わるプロジェクトでは、セレクターの選択方法に一貫性を保つことが重要な課題となります。

各開発者が異なる手法でセレクターを記述すると、テストコード全体の保守性が損なわれてしまいます。新しいメンバーがプロジェクトに参加した際の学習コストも増大するでしょう。

統一されたガイドラインがないまま開発を進めると、プロジェクトが大きくなるにつれてテストコードの品質にばらつきが生じ、結果として保守困難なテストスイートが生まれてしまいます。

解決策

Playwright の推奨 Selector 戦略

Playwright では、要素の選択において明確な優先順位が推奨されています。この戦略に従うことで、保守しやすく壊れにくいテストを実現できます。

第1優先:Role-based Selectors アクセシビリティの観点から最も推奨される手法です。要素の役割に基づいて選択するため、UI の実装詳細に影響されにくい特徴があります。

第2優先:Label や Placeholder による選択 ユーザーが実際に目にするテキスト情報を利用した選択方法です。ユーザー体験と密接に結びついているため、比較的安定しています。

第3優先:Test ID による選択 テスト専用の識別子を使用する方法です。開発とテストの責任を明確に分離でき、意図的な変更以外では壊れることがありません。

mermaidflowchart TD
    A[要素を選択したい] --> B{Role属性はあるか?}
    B -->|Yes| C[getByRole使用]
    B -->|No| D{Label/Placeholderはあるか?}
    D -->|Yes| E[getByLabel/getByPlaceholder使用]
    D -->|No| F{Test IDは設定できるか?}
    F -->|Yes| G[getByTestId使用]
    F -->|No| H[その他のLocator検討]

この図は Playwright における要素選択の判断フローを示しており、より堅牢な方法から順に検討することがわかります。

優先順位付きセレクター選択

実際の開発において、この優先順位をどのように適用するかを具体的に見ていきましょう。

フォーム要素の場合、まず role 属性での選択を試みます。ボタンであれば getByRole('button') を、入力フィールドであれば getByRole('textbox') を使用します。

typescript// Role-based selection(推奨)
await page.getByRole('button', { name: '送信' }).click();
await page.getByRole('textbox', { name: 'メールアドレス' }).fill('test@example.com');

Role で特定が困難な場合は、ラベルテキストによる選択を検討します。

typescript// Label-based selection
await page.getByLabel('パスワード').fill('securepassword');
await page.getByPlaceholder('名前を入力してください').fill('田中太郎');

どちらも使用できない場合に、Test ID を利用します。

typescript// Test ID-based selection
await page.getByTestId('submit-button').click();
await page.getByTestId('email-input').fill('test@example.com');

ベストプラクティス集

Playwright Selectors を効果的に活用するための、実践的なベストプラクティスをご紹介します。

セレクターの組み合わせ活用 複数の選択手法を組み合わせることで、より確実な要素特定が可能になります。

typescript// 複数条件の組み合わせ
await page
  .getByRole('button')
  .filter({ hasText: '削除' })
  .filter({ has: page.locator('.danger-action') })
  .click();

階層関係の活用 親要素から子要素への絞り込みを活用することで、同じような要素が複数存在する場合でも確実に目的の要素を選択できます。

typescript// 階層関係を利用した選択
await page
  .locator('[data-section="user-profile"]')
  .getByRole('button', { name: '編集' })
  .click();

動的コンテンツへの対応 条件付きでの要素選択や、複数候補からの選択など、動的な状況に対応できる柔軟な記述方法を身につけることが重要です。

具体例

Role-based Selectors の実装

Role-based Selectors は、Web アクセシビリティ標準に基づいた最も推奨される選択方法です。実際のコード例を通して理解を深めていきましょう。

ボタン要素の選択 ボタンの選択では、getByRole('button') を基本として、ボタンのテキストや追加属性で絞り込みを行います。

typescript// 基本的なボタン選択
await page.getByRole('button', { name: '保存' }).click();

// 複数のボタンがある場合の絞り込み
await page.getByRole('button', { name: /削除|Delete/ }).click();

// 状態による絞り込み
await page.getByRole('button', { name: '送信', disabled: false }).click();

フォーム要素の選択 入力フィールドやセレクトボックスなど、フォーム関連の要素も role を活用して選択できます。

typescript// テキスト入力フィールド
await page.getByRole('textbox', { name: 'ユーザー名' }).fill('testuser');

// パスワード入力フィールド  
await page.getByRole('textbox', { name: 'パスワード' }).fill('password123');

// セレクトボックス
await page.getByRole('combobox', { name: '都道府県' }).selectOption('東京都');

// チェックボックス
await page.getByRole('checkbox', { name: '利用規約に同意する' }).check();

Text-based Selectors の活用

テキストベースの選択は、ユーザーが実際に目にする内容に基づいているため、直感的で理解しやすい特徴があります。

正確なテキストマッチング 完全一致でのテキスト選択は、最も確実な方法の一つです。

typescript// 完全一致でのテキスト選択
await page.getByText('ログイン').click();

// 大文字小文字を区別しない選択
await page.getByText('LOGIN', { exact: false }).click();

部分一致や正規表現の活用 動的に変化するテキストや、複数のバリエーションがあるテキストに対応できます。

typescript// 部分一致での選択
await page.getByText('件の検索結果').click();

// 正規表現を使用した柔軟な選択
await page.getByText(/\d+件の商品/).click();

// 複数パターンに対応
await page.getByText(/保存|更新|Save/).click();

リンクテキストの選択 ナビゲーションやページ内リンクの選択では、リンクテキストを直接指定することが効果的です。

typescript// リンクテキストでの選択
await page.getByRole('link', { name: 'お問い合わせ' }).click();

// 部分的なリンクテキスト
await page.getByRole('link', { name: /詳細を見る/ }).click();

Test ID を使った安定した選択

Test ID は、テスト専用の識別子として設計されているため、最も安定した選択方法の一つです。

基本的な Test ID の使用 HTML 要素に data-testid 属性を追加し、その値を使って要素を選択します。

typescript// 基本的なTest IDの使用
await page.getByTestId('login-form').locator('input[name="username"]').fill('testuser');
await page.getByTestId('submit-button').click();

// ネストした要素の選択
await page.getByTestId('user-card').getByTestId('edit-button').click();

命名規則の統一 プロジェクト全体で一貫した Test ID の命名規則を採用することが重要です。

typescript// 推奨される命名パターン
await page.getByTestId('header-navigation-menu').click();
await page.getByTestId('product-list-item-1').click();
await page.getByTestId('checkout-payment-form').locator('input[name="cardNumber"]').fill('1234567890123456');

// セクション-機能-要素の形式
await page.getByTestId('dashboard-analytics-chart').isVisible();
await page.getByTestId('settings-profile-save-button').click();

複合条件での要素特定

複雑な UI では、単一の条件だけでは要素を一意に特定できない場合があります。Playwright では複数の条件を組み合わせることで、確実な要素選択が可能です。

フィルターチェーンの活用 複数のフィルター条件を連鎖させることで、段階的に要素を絞り込めます。

typescript// 複数フィルターの組み合わせ
await page
  .getByRole('row')
  .filter({ hasText: '田中太郎' })
  .filter({ has: page.locator('.status-active') })
  .getByRole('button', { name: '編集' })
  .click();

// テーブル内の特定セルの選択
await page
  .getByRole('table')
  .locator('tbody')
  .getByRole('row')
  .filter({ hasText: 'Premium Plan' })
  .getByRole('cell')
  .nth(2) // 3番目のセル
  .click();

条件分岐での動的選択 ページの状態に応じて異なる選択ロジックを適用することも可能です。

typescript// 条件分岐での選択
const isLoggedIn = await page.getByTestId('user-menu').isVisible();

if (isLoggedIn) {
  await page.getByRole('button', { name: 'ログアウト' }).click();
} else {
  await page.getByRole('link', { name: 'ログイン' }).click();
}

// 複数候補からの選択
const primaryButton = page.getByRole('button', { name: '続行' });
const secondaryButton = page.getByRole('button', { name: 'Next' });

await Promise.race([
  primaryButton.waitFor(),
  secondaryButton.waitFor()
]).then(() => {
  return primaryButton.isVisible() ? primaryButton.click() : secondaryButton.click();
});

この章で示した各手法を適切に組み合わせることで、DOM の変更に強く、保守しやすいテストコードを作成できるようになります。

まとめ

壊れにくいテストを書くためのポイント整理

本記事を通して、Playwright の Selectors を活用した壊れにくいテスト作成方法について詳しく解説してまいりました。重要なポイントを整理いたします。

セレクター選択の優先順位

  1. Role-based Selectors(getByRole)を第一選択とする
  2. Label や Placeholder による選択(getByLabel, getByPlaceholder)を次に検討
  3. Test ID(getByTestId)を安定した選択手法として活用
  4. CSS セレクターや XPath は最後の手段として位置づける

実装時のチェックリスト

項目チェックポイント
セマンティック性要素の役割や意味に基づいた選択ができているか
保守性デザイン変更の影響を受けにくい選択方法か
可読性他の開発者が理解しやすい記述になっているか
一貫性プロジェクト全体で統一されたアプローチか
動的対応条件分岐や複合条件での選択に対応できているか

長期的な効果 適切な Selector 戦略を採用することで、以下の効果が期待できます。

  • テストの保守コスト大幅削減
  • CI/CD パイプラインでの安定したテスト実行
  • 新しい開発メンバーの学習効率向上
  • プロダクト品質の継続的な担保

これらのポイントを意識して実装することで、開発チーム全体の生産性向上につながることでしょう。Playwright の強力な Selectors 機能を最大限活用し、より良いテスト環境の構築を目指していただければと思います。

関連リンク

Playwright 公式ドキュメント

アクセシビリティ参考資料

テスト戦略・設計

コミュニティ・学習リソース