SPECIALIST

多様な専門性を持つNRIデジタル社員のコラム、インタビューやインサイトをご紹介します。

BACK

生成AIを活用した Playwright テストの継続運用実践ガイド – 保守性の向上と実行基盤の整備

1. はじめに

こんにちは、NRIデジタルの森です。

近年、E2Eテストの作成は、かつてないほど「始めやすい」ものになりました。
Playwrightに搭載されているCodegen(ブラウザ操作からのコード生成機能)を使えば、GUI 操作のみでテストの骨組みを作ることができますし、最近では自然言語によるテスト作成や修復を支援するアプローチに加え、Test Agentsのように自律的にテストを構成する試みも登場しています。
その意味で、「まず1本、動くテストを作ること」自体のハードルは、大きく下がったと言えるでしょう。

しかし、実際のプロジェクトで課題になりやすいのは、最初の1本を作ることよりも、それを継続的に運用できる状態に保てるか という点です。

画面仕様の変更や文言の差し替え、テストケースの追加、さらには実行環境ごとの差異への対応などが積み重なってくると、作成当初は快調に動いていたテストも、次第にメンテナンスが追いつかなくなり、いわゆる「壊れやすいテスト(Flaky Tests)」へと変貌してしまいます。
特に、自動生成されたコードをそのままベースにしてしまうと、ロケーターの取り方、共通処理の持たせ方、アサーションの書き方、テストデータの管理方法といった設計上の粗さが、後から保守負荷として表面化しやすくなります。

こうした課題に対して、Playwright 公式が示すベストプラクティスは有効な指針になります。
一方で、実際のプロジェクトでは、既存のテストコードや画面仕様、利用しているテストデータに応じて、どのようにベストプラクティスを適用するかを個別に判断する必要があります。

ここで有効なのが、生成AIの活用です。
生成AIを使うことで、既存のテストコードを読み解き、重複処理の切り出し、アサーションの改善、テストデータの外部化といった保守性向上の作業を、プロジェクト固有のシナリオに合わせて効率的に進めることができます。

Playwrightを実運用で使いこなすためには、「いかに素早く作るか」だけでなく、「いかに変更に強く、長く保守できる形へ整えるか」という視点が欠かせません。そして、その整備を人手だけで進めるのではなく、生成AIを活用して継続的に改善していくことが、今後のE2Eテスト運用では重要な選択肢になると考えています。

そこで本記事では、Playwrightテストを継続運用していくうえで、生成AIをどのように活用し、テストコードや実行基盤を整えていくべきかに焦点を当てます。

まず第2章で、Playwrightのベストプラクティスを保守性の観点から整理します。続く第3章では、生成AIを効果的に活用するためのテスト開発プロセスを整理したうえで、第2章の設計指針をもとに、生成AIを「既存テストを保守しやすい形へ改善していくための伴走者」として活用する具体的な手法を解説します。そして第4章では、整えたテストをCI/CDの中で継続的に回していくための実行基盤として、Azure App TestingのPlaywright Workspaces の活用方法を解説します。

本記事が、Playwrightテストを「作って終わり」にせず、価値ある資産として育てていくためのヒントになれば幸いです。

2. Playwrightにおけるベストプラクティスの整理

前章で触れたように、Playwright では Codegen やAIツールの進化により、テスト作成の初動は極めてスムーズになりました。しかし、生成されたコードをそのまま「資産」として運用し続けられるかは別問題です。生成AIを効果的に活用するためにも、まずは何を目指してテストコードを改善すべきかという判断軸が必要になります。

実運用に耐えうる、つまり「仕様変更に強く、失敗原因が特定しやすい」状態を維持するためには、Playwright公式が掲げるベストプラクティスを指針とするのが近道です。これらは単なる作法集ではなく、メンテナンスコストを最小化するための「テスト設計のチェックリスト」として機能します。
特に重要なポイントは、以下の4つに集約されます。

2-1. ユーザーに見える振る舞いを検証する

実装の詳細ではなく、「ユーザーが実際に何を見て、どう操作するか」を対象にテストする原則です。
これは、要素を特定する「ロケーター」の選び方だけでなく、その要素がどうなったかを確認する「アサーション」の書き方にも共通する原則です。

たとえば、DOM構造やCSSクラス名に依存したロケーターや、内部状態の値を直接比較するようなアサーションは、画面の見た目や機能が変わっていなくても、実装の都合だけでテストが壊れてしまう原因になります。

一方で、ユーザーが認識できるラベルやロール(役割)を基準に要素を探し、「期待するメッセージが画面に表示されたか」「ボタンが操作可能になったか」といったユーザー視点の変化をアサーションで検証していけば、テストの意図が明確になります。

2-2. テストを互いに独立させる

各テストケースが他の実行結果に依存せず、「単独で実行・再現できる状態」を保つ、という考え方です。

ここでいう独立性とは、単にテストファイルを分けることではなく、各テストが実行に必要な前提条件を自分自身で満たせる状態にしておくことを指します。たとえば、データ更新のシナリオを実行するには、更新対象となるデータがあらかじめ存在している必要があります。この前提を別のテストで作成されたデータに依存してしまうと、テストの実行順序や他テストの失敗状況によって結果が変わってしまいます。そのため、テストの独立性を保つうえでは、「前提データをどのように用意するか」をあらかじめ設計しておく必要があります。

2-2-1. 前提データの準備方法

前提データの用意方法には、大きく2つの考え方があります。それぞれに向いている場面と注意点があるため、テスト対象の性質や実行環境の運用方針に応じて使い分けます。

① E2E テストの中で前提データを作成する

1つ目は、E2E テストの中で前提データを作成する方法です。
たとえばデータ更新テストであれば、テストの前処理としてデータ登録を行い、そのデータを対象に更新処理を検証します。この方法は、次のような場面に向いています。

  • 前提データを API 経由などで短時間に作成できる
  • テストごとに一意なデータを用意したい
  • テスト環境をまたいでも再現しやすい状態にしたい
  • 事前データセットへの依存を減らしたい

一方で、実行のたびにデータが増え続ける点には注意が必要です。長期運用では、データ量の増加による性能劣化や、採番方式・データ保持期間によっては内部 IDの増加に伴う想定外の影響が発生する可能性があります。そのため、この方法を採用する場合は、あわせて次のような運用方針を検討します。

  • テストの後処理としてデータ削除を行う
  • 定期的にテスト環境をリセットする
  • 実行 ID やタイムスタンプを付与し、テストデータを識別しやすくする
  • テストデータの保持期間や削除ルールを決めておく
② 事前データセットとしてあらかじめ用意する

2つ目は、事前データセットとして必要なデータを用意しておく方法です。
必要なデータ、特定権限を持つユーザーなどをテスト環境にあらかじめ投入しておき、各テストはその固定データを前提に実行します。この方法は、次のような場面に向いています。

  • 前提データの作成負荷が高い
  • 複雑な業務状態を毎回 E2E の前処理で再現するのが難しい
  • テスト開始時点のデータ状態を固定したい
  • テスト実行時間を短縮したい

一方で、事前データセットを利用する場合は、データの初期化方法を明確にしておく必要があります。
また、事前データの内容が仕様変更に追従していないと、テストの失敗原因がアプリケーションの不具合なのか、テストデータの古さなのかを切り分けにくくなります。そのため、この方法を採用する場合は、次の点をあらかじめ決めておくとよいでしょう。

  • テスト実行前にデータをどの状態へ戻すか
  • 自動テスト専用環境を用意するか
  • 事前データセットを誰が、どのタイミングで更新するか
使い分けの考え方

使い分けの目安としては、前提データを短時間で作成でき、テストごとに一意な状態を持たせたい場合は、E2Eテスト内または fixture内で作成する方法が適しています。一方で、作成に時間がかかるデータや、複雑な業務状態を再現する必要があるデータは、事前データセットとして用意する方が現実的です。

いずれの場合も、テスト実行後にデータをどう扱うかを運用ルールとして決めておくことが重要です。初期化するのか、蓄積を許容するのか、定期的にリセットするのかをあらかじめ決めておくことで、テストの独立性と継続運用のしやすさを両立しやすくなります。
判断時の観点をまとめると、以下のようになります。

                        
観点 ① E2E 内で前提データを作成② 事前データセットを利用
向いている場面 軽量なデータを短時間で作成できる場合 複雑な業務状態を再現する必要がある場合
再現性 テスト単体で前提条件を満たしやすい 初期データが正しく維持されていれば安定しやすい
実行時間 前処理の分だけ長くなりやすい 短縮しやすい
注意点 データが増え続ける可能性がある データ初期化や専用環境が必要になりやすい
運用上の対策 後片付け、定期リセット、一意な命名 初期化手順、データ更新ルール、手動データとの分離

2-2-2. ユーザーID体系とデータ競合に配慮する

ユーザー ID 体系の設計も、テストの独立性を保つうえで重要な観点です。

ユーザー状態を変更しない参照系のテストであれば、共通のテストユーザーを利用しても問題になりにくい一方で、データの登録・更新など、ユーザー状態や業務データによって結果が左右されるケースでは、同一ユーザーを複数テストで共有すると、テスト同士が干渉する可能性があります。

そのため、更新を伴うテストでは、テストケースごとに利用するユーザーを分ける設計が有効です。
また、テスト実行時にユーザーやデータを動的に作成する場合は、ユーザー ID やメールアドレスに実行 IDやタイムスタンプなどを含めることで、一意性を確保しやすくなります。

特に並列実行を前提とする場合は、同じユーザーや同じ業務データを複数のテストが同時に更新しないようにする必要があります。ユーザーを固定で用意する場合でも、「読み取り専用ユーザー」「更新系テスト用ユーザー」「管理者ユーザー」のように用途別に分けておくと、意図しない状態競合を避けやすくなります。

2-3. サードパーティの依存関係をテスト対象に含めない

「自分たちが制御できる範囲のみ」をテストする、という考え方です。
E2Eテストでは「本番に近い形ですべてを繋げて確認したい」と考えがちですが、制御不能な外部要因が増えるほど、テストは不安定(Flaky)になります。

たとえば、外部の解析タグ、決済サービス、サードパーティ APIなどをそのままテストに含めると、ネットワークの瞬断や接続先の仕様変更、応答遅延によってテストが失敗しかねません。これでは、失敗の原因が自システムにあるのか、外部にあるのか判別がつきにくくなってしまいます。

2-4. データ管理を徹底する

テストで扱う値を「常に同じ前提で扱える状態(冪等性の担保)」にしておく、という考え方です。

ここでいうデータには、認証情報だけでなく、フォームの入力値や期待値となる文言も含まれます。これらが各テストにハードコードされていると、仕様変更や環境差分が生じた際に修正が漏れ、メンテナンスの足かせとなってしまいます。

認証情報は環境変数や設定ファイルへ、入力値や期待値はテストデータ用のヘルパーへ切り出すなど、値をパラメータとして外部から注入できる構造にしておくことが、長期的な運用では不可欠です。

3. 生成AIを活用した Playwright テストの保守性向上

ここからはまず、生成AIを効果的に活用するためのテスト開発プロセスを整理します。
そのうえで、前章で整理した4つのベストプラクティスに沿って、生成AIを活用しながら Playwrightのテストコードを「保守しやすい資産」へと磨き上げるための4つの改善手法を解説していきます。

本記事で想定している「生成AI」は、テストコードの文脈を理解し、リファクタリング方針の提案から複数ファイルにまたがる修正までをシームレスに行えるものです。

本記事の主眼は、ツールの操作方法ではなく、「生成AIを使いこなし、Playwright のベストプラクティスを既存コードにどう落とし込むか」という点にあります。紹介するプロンプト例や方針は、環境を問わず応用可能な「実践的な思考プロセス」としてご活用ください。

また、以降紹介するプロンプト例には、ファイル名やディレクトリ名、ドメイン名、環境変数名など、プロジェクトごとに変更が必要な値が含まれます。「プロンプト例利用時の注意点」を確認し、自環境に合わせて変更したうえで利用してください。

3-0. 生成AIを効果的に用いるためのテスト開発プロセス

生成AIをE2Eテスト開発に導入する際に重要な考え方は、「すべての工程を生成AIに委ねない」ことです。

いきなり完成形のテストコードを生成させるのではなく、まずPlaywrightの標準機能を用いて、実際のブラウザ操作や画面状態に基づくベースラインを作成します。そのうえで、生成AIを用いて「保守性の高い構造」へと整えていく進め方が、現実的かつ有効です。

本記事では、Playwright公式のベストプラクティスを踏まえ、生成AIを効果的に組み込むためのプロセスを以下の4つのステップで整理します。

① Codegen による「ベースライン」の作成

まず、新しい画面や業務フローに対しては、Codegenを使って実際のブラウザ操作を記録します。

この段階で生成されるコードは、最終的なテストコードというよりも、シナリオの流れ、必要な操作、画面上で利用できるロケーターを把握するための「ベースライン」として扱います。

特に、生成AIにゼロから画面構造や操作手順を推論させるよりも、実際のブラウザ操作から得られたコードを出発点にすることで、画面上の事実に即したテスト設計を行いやすくなります。Codegenで作成したベースラインをもとに、その後の整理やリファクタリングを生成AIに委ねることで、効率と正確性を両立しやすくなります。

② ユーザー体験(UX)と期待結果の定義

次に、作成したベースラインをもとに、検証の目的を改めて整理します。

単なる「操作手順のなぞり」にならないよう、「どのユーザー体験を保証したいのか」「期待する結果は画面上にどのように表れるのか」を明確にします。この工程を挟むことで、テストが実装の詳細ではなく、ユーザーに見える振る舞いを検証するものとして整理しやすくなります。

この段階で、ロケーターについても確認しておきます。特に、「どのロケーターが適切か」「複数の要素にマッチしていないか」といった判断は、生成AIの推論だけに頼るのではなく、PlaywrightのPick Locatorを使って実画面と照らし合わせるのが有効です。

RoleやTextなど、ユーザー視点に近い属性を優先しつつ、狙った要素が一意に特定できているかを確認しておくことで、後続のリファクタリングでも不安定なテストを生みにくくなります。

③ 生成AIを用いた改善・展開

整理した検証目的に基づき、生成AIを用いてコードを構造的に改善します。

たとえば、手動待機をWebファーストなアサーションへ置き換える、重複する前処理をfixture/setupへ切り出す、外部サービスへの依存をモック化する、認証情報や期待値をテストデータとして分離する、といった改善です。(詳細は次節以降で解説します。)

また、整えた構造をもとに、正常系・異常系・複数データパターンなどの類似シナリオへ展開する場面でも、生成AIを活用できます。ただし、その場合もテスト同士の独立性やデータの制御性を損なわないよう、検証目的と前提条件を人間が確認することが重要です。

④ Playwright標準機能を用いた「事実ベース」の確認

生成AIによる改善後も、実際の画面や実行結果に基づく確認は欠かせません。
どのロケーターが適切か、複数の要素にマッチしていないか、失敗時に画面上で何が起きていたのかといった「事実」の確認は、Playwrightの標準機能を使って判断するのが確実です。

特に、ロケーターの確認にはPick Locator、失敗原因の調査にはTrace Viewer を活用します。Trace Viewer では、各アクション実行時のスナップショット、ネットワーク通信、コンソールログ、ソースコードの実行行などを事実ベースで振り返ることができます。

AI による推論だけで失敗原因を判断すると、実際の画面状態とは異なる仮説に基づいて修正してしまう可能性があります。Trace Viewerで得られるエビデンスを突き合わせることで、失敗原因をより迅速かつ正確に特定できます。

このように、生成AIを効果的に活用するためには、まずPlaywrightの標準機能で事実に基づくベースラインを作成し、検証したいユーザー体験を人間が整理したうえで、生成AIによる構造改善やシナリオ展開につなげることが重要です。
次節以降では、このプロセスのうち、生成AIが特に力を発揮する③の改善・展開について、Playwrightのベストプラクティスに沿った4つの観点から具体的に見ていきます。

3-1. アサーションを「ユーザーに見える振る舞い」に寄せる

「2-1. ユーザーに見える振る舞いを検証する」のベストプラクティスに従うためには、テストが実装内部の状態ではなく、ユーザーが画面上で認識できる変化を検証する形になっていることが重要です。

ここでは、アサーション(検証)の質を高めるリファクタリングを行います。これにより、「そのテストが何を保証しようとしているのか」を、コードを読むだけで直感的に理解できるようになります。

具体的には、waitForTimeout(手動待機)や、DOMから値を取得して比較するような実装依存の処理を、Playwright推奨の「Webファーストなアサーション(自動リトライを伴う非同期アサーション)」に置き換えていきます。

プロンプト例

# 命令
Playwright テストコードを診断し、ユーザー視点での「振る舞いの検証」を重視した構造にリファクタリングしてください。

# リファクタリングの方針
実装詳細に依存した不安定な検証を排除し、Playwright の強みである「自動リトライ」を活かした堅牢なテストへ改善します。
以下のケースに該当する記述がある場合は、推奨される形式へ書き換えてください。

1. **手動待機や即時比較の改善**: 
   - `waitForTimeout` による時間指定の待機や、`textContent()` を取得した直後の値比較(即時比較)が使用されている箇所は、`expect(locator).toBeVisible()` や `expect(locator).toHaveText()` などの「Webファーストなアサーション」へ置き換えてください。
2. **検証の意図の明確化**: 
   - 1つのテストケースが「どのようなユーザー体験を保証しようとしているか」が直感的に伝わるよう、アサーションの構成や順序を整理してください。

# 制約事項
- **既存ロケーターの維持**: 今回の修正では要素特定(locator)のロジック自体には手を加えず、検証(assertion)の記述方法の改善に集中してください。
- **意味の保持**: 修正によってテストの本来の目的や検証範囲が変わらないようにしてください。

改善前のコード例

以下の例は、「秒数指定による手動待機後、一度変数に取ってから比較する」という命令的な書き方になっています。これではリトライが効かず、画面遷移や描画のわずかな遅延でテストが失敗してしまう「Flaky」な状態を招きやすいです。

import { test, expect } from '@playwright/test';

test('注文完了後に完了画面へ遷移すること', async ({ page }) => {
  await page.goto('/checkout');
  await page.locator('[data-testid="submit-order"]').click();

  // 手動待機:画面遷移が遅れると、下の textContent() が空で取得されるリスクがある
  await page.waitForTimeout(2000);

  // 命令的なアサーション:この瞬間に要素がないと即座に失敗し、リトライもされない
  const title = await page.locator('.complete-title').textContent();
  expect(title).toBe('ご注文ありがとうございました');
});

改善後のコード例

ユーザーが確認できる「表示状態」を直接検証する形へ変換されました。Playwrightのアサーションは、要素が表示されるまで自動で待機(リトライ)するため、不安定な手動待機を排除しつつ、可読性も向上しています。

import { test, expect } from '@playwright/test';

test('注文完了後に完了画面へ遷移すること', async ({ page }) => {
  await page.goto('/checkout');
  await page.locator('[data-testid="submit-order"]').click();

  // 宣言的なアサーション:検証ロジックを Web ファーストに変換
  // 要素が現れ、テキストが一致するまで Playwright が自動で待機・リトライを行う
  await expect(page.locator('.complete-title'))
    .toHaveText('ご注文ありがとうございました');
});

3-2. テストの独立性を保つためにfixture/setupを活用する

「2-2. テストを互いに独立させる」のベストプラクティスに従うためには、各テストが実行に必要な前提条件を事前に満たせる構造にしておくことが重要です。ここでは、2-2で整理した前提データの準備やユーザーIDの分離といった設計方針を実装する際に、生成AIをどのように活用できるかを見ていきます。

Playwrightのfixturesetup projectは、前提条件の準備を各テストから切り出し、独立性を保ったまま再利用しやすくするための仕組みとして活用できます。これは単なる「共通処理の切り出し」ではなく、認証済み状態、テスト用データ、API モックなどを、各テストが安定して利用できる形に整えるための実装です。
ただし、両者は同じ目的で使うものではありません。どのような前提条件を準備したいのかによって、使い分けることが重要です。

custom fixture:テストごとに必要な「前提データ」や「処理」を準備する

custom fixtureは、特定のテストだけが必要とする前提条件を準備する場合に適しています。たとえば、更新処理のテストに必要なデータ、外部APIのモックなどです。

2-2-1 で述べた「E2E テストの中で前提データを作成する」方針を採る場合、custom fixtureにデータ作成処理を切り出すと、各テストは必要な前提データを自分自身で準備できるようになります。これにより、別のテストで作成されたデータや、事前データセットの状態に依存せず、単独で実行・再現しやすいテストになります。

また、fixtureにはテスト終了後の後片付けも含めることができるため、データ削除や一意な命名といった運用方針を fixture側に閉じ込めることで、テスト本体は「何を検証するか」に集中できます。

setup project:複数テストで共通利用できる「状態」を準備する

一方で setup projectは、複数のテストで共通して利用できる状態を、テスト本体の実行前に一度だけ準備したい場合に適しています。

たとえば、多くのテストがログイン後の画面を起点にする場合、各テストで毎回ログイン処理を実行すると、テスト時間が長くなり、ログイン画面の変更による修正箇所も増えてしまいます。このような場合は、setup projectでログイン処理を実行し、PlaywrightのstorageState として認証済み状態を保存しておくことで、各テストをログイン済みの状態から開始できます。

一方で、setup projectで作成した状態は複数のテストから再利用されるため、テスト中にその状態を変更するケースには注意が必要です。2-2-2で述べたように、更新を伴うテストではユーザーやデータの競合が起こりうるため、setup projectは主に「共有してもテスト同士が干渉しにくい状態」の準備に使い、更新系のテストでは custom fixtureでデータやユーザーを分離する構成が有効です。

また、storageStateには Cookieやヘッダーなど、テストアカウントとしてアクセス可能な情報が含まれる場合があるため、保存先となるplaywright/.authディレクトリは.gitignoreに追加し、認証状態ファイルをリポジトリにチェックインしないよう注意が必要です。

使い分けの考え方

両者の使い分けとして、custom fixtureは「各テストが独立して成立するために必要な前提条件」、setup projectは「複数のテストで共有できる前提状態」を準備するもの、と整理すると分かりやすいでしょう。

                                       
準備したいもの 適した仕組み注意点
複数テストで共通利用できる状態 setup project ログイン済み状態、共通の初期設定状態を更新するテストでは干渉に注意する
特定テストに必要な前提データ custom fixture 更新対象の注文データ、解除対象の契約データ 作成データの削除やリセット方針を決める
特定テストに必要な処理 custom fixture API モック、画面遷移、テスト用ユーザー作成 テストの意図が隠れないよう命名を明確にする

この判断基準をプロンプトに明示することで、生成AIにも単なる重複削減ではなく、「テストの独立性を保つための前提条件管理」としてリファクタリングさせやすくなります。

プロンプト例

# 命令
プロジェクト内の Playwright テストコードを分析し、各テストが他のテストの実行結果や副作用に依存せず、単独で実行・再現できる構造へリファクタリングしてください。

# 分析観点
1. **前提条件の洗い出し**
   - 各テストが必要としている前提条件を洗い出してください。
   - 例: 認証済み状態、テスト用ユーザー、更新対象データ、API モックなど。
2. **前提条件の分類**
   - 複数テストで共通利用できる「状態」なのか、特定テストだけが必要とする「前提データ」または「処理」なのかを分類してください。
   - 共有してもテスト同士が干渉しにくい状態は `setup project` の利用を検討してください。
   - テストごとに一意に準備すべきデータや、特定テストだけで使う処理は `custom fixture` の利用を検討してください。
3. **前提データの準備方法の判断**
   - 前提データを E2E テスト内または fixture 内で作成すべきか、事前データセットとして用意すべきかを判断してください。
   - 短時間で作成でき、テストごとに一意な状態が必要なデータは、テスト内または fixture 内で作成する方針を優先してください。
   - 作成負荷が高いデータや、複雑な業務状態を再現する必要があるデータは、事前データセットとして用意する方針を検討してください。
   - いずれの場合も、テスト実行後にデータを削除するのか、初期化するのか、定期的にリセットするのかを考慮してください。
4. **ユーザー ID / テストデータの競合回避**
   - 複数のテストが同じユーザーや同じ業務データを更新して競合しないようにしてください。
   - 参照系のテストと、更新を伴うテストで利用するユーザーを分けてください。
   - 並列実行を想定し、必要に応じてテストケースごとに利用するユーザーやデータを分離してください。
   - 動的にユーザーやデータを作成する場合は、実行 ID やタイムスタンプなどを利用し、一意に識別できる命名にしてください。

# 判断基準
以下の基準に基づき、最適な配置先を選択してください。
- **認証済み状態など、複数テストで共通利用できる状態を準備する場合**: `setup project` を優先。
- **更新対象データ、注文済みデータ、API モックなど、特定テストの前提条件を準備する場合**: `custom fixture` を優先。
- **事前データセットを利用する場合**: 参照方法、初期化方法、データ更新ルールを明確にしてください。
- **最優先事項**: テスト同士の独立性を損なわないこと。別テストの実行結果や実行順序に依存しない構造にしてください。

# ルール
1. **setup project**
   - 認証済み状態の保存には、`storageState` を使用してください。
   - `playwright/.auth` は `.gitignore` に追加し、認証状態ファイルをリポジトリにチェックインしない構成にしてください。
   - 認証状態ファイルには Cookie やヘッダーなどの機密情報が含まれる可能性があるため、平文の成果物として扱わないでください。
   - 共有する状態がテスト中に変更される場合は、ユーザーやデータを分離する方針を提案してください。
2. **custom fixture**
   - fixture ファイルを新規作成または更新し、役割が明確な名前で fixture を定義してください。
   - fixture では、テストに必要な前提条件を明示的に準備してください。
   - テスト終了後の後片付けが必要な場合は、その処理も含めてください。
   - 作成するデータには、必要に応じて実行 ID やタイムスタンプなどを付与し、一意に識別できるようにしてください。
3. **DRY 原則の適用**
   - 各 spec ファイルからは重複する前処理を削除し、共通化された仕組みを呼び出す記述に簡潔化してください。
   - ただし、共通化によってテストの意図や前提条件が読み取りにくくならないようにしてください。
4. **意味の保持**
   - 修正によってテストの本来の目的や検証範囲が変わらないようにしてください。

※ プロンプト例利用時の注意点

  • fixtureファイルの配置場所やファイル名は、自環境のテスト構成に合わせて変更してください。
  • 認証済み状態を共有できるかどうかは、テストデータやアカウントの干渉有無に応じて判断してください。
  • 前提データをE2E内で作成するか、事前データセットとして用意するかは、データ作成コスト、実行時間、初期化方針、自動テスト専用環境の有無を踏まえて判断してください。
改善前のコード例

以下では、更新対象となる注文データがあらかじめ存在していることを前提にしています。
この状態では、事前データが削除されたり、別のテストによって状態が変更されたりすると、テスト結果が不安定になります。

// tests/orders.spec.ts
import { test, expect } from '@playwright/test';

test('注文の配送先住所を更新できること', async ({ page }) => {
  // 事前に存在している注文 ID に依存している
  const orderId = 'ORD-1001';

  await page.goto(`/orders/${orderId}`);

  await page.getByRole('button', { name: '配送先を変更' }).click();
  await page.getByLabel('住所').fill('東京都千代田区1-1-1');
  await page.getByRole('button', { name: '保存' }).click();

  await expect(page.getByText('配送先を更新しました')).toBeVisible();
  await expect(page.getByText('東京都千代田区1-1-1')).toBeVisible();
});
改善後のコード例(custom fixtureの場合)

更新対象となる注文データを fixtureで作成し、テストが必要な前提条件を自分で満たせるようにしました。前提データの作成と後片付けをテストごとに fixture化することで、他のテストや事前データに依存しない、独立したシナリオとして実行できるようになります。

// tests/fixtures/orders.ts
import { test as base, expect } from '@playwright/test';

type Order = {
  id: string;
  itemName: string;
};

type OrderFixtures = {
  orderForUpdate: Order;
};

export const test = base.extend<OrderFixtures>({
  orderForUpdate: async ({ request }, use) => {
    // テストごとに更新対象となる注文データを作成する
    const response = await request.post('/api/test/orders', {
      data: {
        itemName: 'ステンレスボトル',
        shippingAddress: '東京都中央区1-1-1',
      },
    });

    const order = await response.json();

    await use({
      id: order.id,
      itemName: order.itemName,
    });

    // 必要に応じて、テスト終了後に作成データを削除する
    await request.delete(`/api/test/orders/${order.id}`);
  },
});

export { expect } from '@playwright/test';
// tests/orders.spec.ts
import { test, expect } from './fixtures/orders';

test('注文の配送先住所を更新できること', async ({ page, orderForUpdate }) => {
  await page.goto(`/orders/${orderForUpdate.id}`);

  await page.getByRole('button', { name: '配送先を変更' }).click();
  await page.getByLabel('住所').fill('東京都千代田区1-1-1');
  await page.getByRole('button', { name: '保存' }).click();

  await expect(page.getByText('配送先を更新しました')).toBeVisible();
  await expect(page.getByText('東京都千代田区1-1-1')).toBeVisible();
});
補足:setup projectを使う場合

認証済み状態のように、複数のテストで共通して利用できる状態は、setup projectで準備できます。
storageStateとして保存しておけば、各テストをログイン済みの状態から開始できます。

// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
import path from 'path';
import { accounts } from './config/accounts';

// storageState には Cookie やヘッダーなどの機密情報が含まれる可能性があるため、
// playwright/.auth は .gitignore に追加し、リポジトリにチェックインしない
const authFile = path.join(__dirname, '../playwright/.auth/user.json');

setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('メールアドレス').fill(accounts.user.email);
  await page.getByLabel('パスワード').fill(accounts.user.password);
  await page.getByRole('button', { name: 'ログイン' }).click();

  await expect(page.getByRole('navigation')).toBeVisible();
  await page.context().storageState({ path: authFile });
});
// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'e2e',
      use: {
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup'],
    },
  ],
});

3-3. サードパーティ依存を切り離す

「2-3. サードパーティの依存関係をテスト対象に含めない」のベストプラクティスに従うためには、テスト対象を自分たちが制御できる範囲に限定し、外部 APIや他社サービスの状態にテスト結果が左右されないようにすることが重要です。

E2Eテストが不安定になる主な要因の1つは、外部のレコメンドエンジンや決済、解析タグといった「自システム外」のシステムの遅延や仕様変更です。これらをそのままテストに含めると、失敗原因が自システムにあるのか、外部サービスにあるのかを切り分けにくくなります。

Playwrightのネットワーク機能を用いれば、通信をインターセプトしてモックに差し替えたり、テストの本質ではない通信(広告や解析)を一括で遮断することができます。

プロンプト例

# 命令
Playwright テストコードにおいて、外部サービスへの依存を遮断し、テスト対象の責務を明確にするリファクタリングを行ってください。

# 背景
外部 API や解析タグへの依存は、テストの不安定化や速度低下の原因となります。
Network API を活用し、自己が管理するドメイン以外の通信を制御する構成に変更してください。

# ルール
1. **境界線の定義**: 以下のドメイン以外への通信はすべて「外部依存」とみなします。
   - `app.example.local`
   - `api.example.local`
2. **モック化と遮断**: 
   - 検証に不可欠な外部 API は、fixture 内で `route.fulfill()` を用いてモック化してください。
   - テストの本質に関わらない通信(analytics 等)は、`route.abort()` で遮断してください。
3. **構造の分離**: 通信制御ロジックは `fixtures/network.ts` 等の別ファイルへ切り出し、spec 側はビジネスロジックの検証に集中させてください。
4. **最小限のデータ**: モックデータは検証に必要な最小構成で定義してください。
5. **意味の保持**: 修正によってテストの本来の目的や検証範囲が変わらないようにしてください。

※ プロンプト例利用時の注意点

  • モック化すべきドメインは、自環境の通信内容に合わせて追加・変更してください。
  • 通信制御ロジックを切り出す fixture ファイルの配置場所やファイル名は、自環境のテスト構成に合わせて変更してください。
  • モックデータの内容は、各テストで検証したい表示内容や期待値に合わせて変更してください。
改善前のコード例

テスト実行時に、外部のレコメンド API や解析タグ等をそのまま呼び出しているケースです。これでは、外部サービスのわずかな瞬断や仕様変更が、テストの不安定性に直結してしまいます。

// tests/product.spec.ts
import { test, expect } from '@playwright/test';

test('おすすめ商品が表示されること', async ({ page }) => {
  // ページ内部で以下のような通信が発生するとする:
  // - https://recommend.vendor.example/api/items
  // - https://analytics.vendor.example/collect
  await page.goto('https://app.example.local/products/1');

  await expect(page.getByRole('heading', { name: 'おすすめ商品' })).toBeVisible();
  await expect(page.getByText('関連商品A')).toBeVisible();
});
改善後のコード例

通信制御(モック化や遮断)のロジックを fixtureへ分離しました。
これにより、各テストは「特定のデータが与えられたとき、UIがどう振る舞うか」という本質的なシナリオ検証に専念できるようになります。

// tests/fixtures/network.ts
import { test as base } from '@playwright/test';

type NetworkFixtures = {
  mockExternalDependencies: () => Promise<void>;
};

export const test = base.extend<NetworkFixtures>({
  mockExternalDependencies: async ({ page }, use) => {
    await use(async () => {
      // 本質でない外部通信は遮断
      await page.route('https://analytics.vendor.example/**', route => route.abort());

      // 外部レコメンド API は mock 化
      await page.route('https://recommend.vendor.example/**', async route => {
        await route.fulfill({
          status: 200,
          contentType: 'application/json',
          body: JSON.stringify({
            items: [{ id: 'a', name: '関連商品A' }],
          }),
        });
      });
    });
  },
});
// tests/product.spec.ts
import { expect } from '@playwright/test';
import { test } from './fixtures/network';

test('おすすめ商品が表示されること', async ({ page, mockExternalDependencies }) => {
  // 外部依存の切り離しを fixture 経由で適用
  await mockExternalDependencies();

  await page.goto('https://app.example.local/products/1');

  await expect(page.getByRole('heading', { name: 'おすすめ商品' })).toBeVisible();
  await expect(page.getByText('関連商品A')).toBeVisible();
});

3-4. 認証情報やテストデータをパラメータとして切り出す

「2-4. データ管理を徹底する」のベストプラクティスに従うためには、認証情報、入力値、期待値といったテストデータを、常に同じ前提で扱える状態にしておくことが重要です。

テストデータが各テストにハードコードされていると、仕様変更や環境差分への追従が困難になってしまいます。データが各所に散在する状態は、修正漏れを招き、将来的なメンテナンス負荷を増大させる要因です。

ただし、認証情報については、単に別ファイルへ切り出せばよいわけではありません。テスト用アカウントであっても、メールアドレスやパスワードを設定ファイルに平文で保存することは避け、CI/CD シークレットやシークレット管理サービスから実行時に注入する構成とします。

一方で、フォームの入力値や期待値となる文言など、機密情報ではないテストデータは、テストデータ用のヘルパーファイルへ切り出すことで再利用しやすくなります。

このように、データとロジックを分離し、「パラメータとして管理・再利用できる構造」へとリファクタリングすることで、テストコードを「検証シナリオ」という純粋なロジックに集中させることが可能になります。

プロンプト例

# 命令
Playwright の spec ファイル内にハードコードされている値を抽出し、パラメータとして外部管理する構造にリファクタリングしてください。

# 目的
仕様変更や実行環境の差分に対し、コードを修正せずにデータを差し替えられる保守性を確保します。
また、認証情報をテストコードや設定ファイルに平文で保存しない構成にすることで、セキュリティリスクを低減します。

# ルール
1. **認証情報の扱い**:
   - メールアドレス、パスワード、API キー、トークンなどの認証情報は、spec ファイルや設定ファイルに平文で保存しないでください。
   - 認証情報は、CI/CD シークレット、またはシークレット管理サービスから実行時に注入される環境変数を参照する構成にしてください。
2. **入力値・期待値の分離**:
   - フォーム入力値や期待値など、機密情報ではないテストデータは `test-data.ts` 等のヘルパーファイルへ切り出してください。
   - ファイル名や配置場所は、自環境のプロジェクト構成に合わせて変更してください。
3. **spec の簡潔化**:
   - spec ファイルにハードコードされている値を排除し、外部から `import` した定数、または環境変数から取得した値を使用するように書き換えてください。
4. **型安全の確保**:
   - TypeScript を用い、定義したデータ構造に適切な型を付与してください。
5. **意味の保持**:
   - 修正によってテストの本来の目的や検証範囲が変わらないようにしてください

※ プロンプト例利用時の注意点

  • ファイル名やディレクトリ名、環境変数名は、自環境のディレクトリ構成や命名規則に合わせて変更してください。
  • 入力値・期待値として切り出すデータ項目は、対象シナリオに合わせて変更してください。
改善前のコード例

認証情報や、テスト対象となる具体的な期待値がテストに直接書き込まれています。このままでは、テスト用のアカウントを切り替えたり、期待する表示内容を変更したりするたびに、対象となるすべてのテストを直接修正しなければなりません。

import { test, expect } from '@playwright/test';

test('注文履歴に対象の注文が表示されること', async ({ page }) => {
  await page.goto('/login');

  // 認証情報が spec にハードコードされている
  await page.getByLabel('メールアドレス').fill('user@example.com');
  await page.getByLabel('パスワード').fill('Password123!');
  await page.getByRole('button', { name: 'ログイン' }).click();

  await page.goto('/orders');

  // テストデータも spec にハードコードされている
  await expect(page.getByText('注文番号: ORD-1001')).toBeVisible();
  await expect(page.getByText('ステンレスボトル')).toBeVisible();
});
改善後のコード例

データを設定ファイルへと分離しました。各テストは「どのデータセットを使用するか」を宣言するだけになり、テストの意図(シナリオ)がより鮮明に読み取れるようになります。

// tests/config/accounts.ts
export const accounts = {
  defaultUser: {
    // 認証情報は CI/CD シークレット等から実行時に注入される環境変数を参照
    email: process.env.E2E_USER_EMAIL,
    password: process.env.E2E_USER_PASSWORD,
  },
};
// tests/config/test-data.ts
export const orderHistoryData = {
  orderId: 'ORD-1001',
  expectedItemName: 'ステンレスボトル',
};
// tests/orders.spec.ts
import { test, expect } from '@playwright/test';
import { accounts } from './config/accounts';
import { orderHistoryData } from './config/test-data';

test('注文履歴に対象の注文が表示されること', async ({ page }) => {
  await page.goto('/login');

  // spec は「どの値を使うか」を読むだけにする
  await page.getByLabel('メールアドレス').fill(accounts.defaultUser.email);
  await page.getByLabel('パスワード').fill(accounts.defaultUser.password);
  await page.getByRole('button', { name: 'ログイン' }).click();

  await page.goto('/orders');

  await expect(page.getByText(`注文番号: ${orderHistoryData.orderId}`)).toBeVisible();
  await expect(page.getByText(orderHistoryData.expectedItemName)).toBeVisible();
});

4. Playwright テスト運用を支えるAzure実行基盤

4-1. 実行基盤の必要性

ここまで見てきたように、Playwrightテストは「書ける」だけでは十分ではなく、継続的かつ安定的に実行できてはじめて、運用上の真の価値が生まれます。

しかし、実行基盤を自前で用意する場合、複数のブラウザやOSを安定稼働させるための計算資源を確保し続ける必要があり、コストが膨らみやすくなります。また、ブラウザやOS・ライブラリの更新、実行結果の保管など、煩雑な運用オーバーヘッドも無視できません。

つまり、E2Eテストを継続運用するうえでは、テストコード自体の保守性だけでなく、それを支える実行基盤をいかに安定的に・効率よく確保するかが重要な論点となります。

4-2. Playwright Workspaces とは

Playwright Workspaces(旧称:Microsoft Playwright Testing)は、Azure App Testingの中で PlaywrightベースのE2Eテストを実行するためのフルマネージドサービスです。既存のPlaywrightテストを大きく書き換えることなく、クラウドホストされたブラウザ上でテストを実行できるようになっており、LinuxやWindows上の主要ブラウザをまたいだ検証にも標準で対応しています。

Playwright Workspacesの最大の強みは、「Playwright テストの既存資産を維持しながら、実行基盤の運用を Azure 側に委ねられる」という点にあります。

  • 運用負荷の軽減:
    ブラウザ実行環境の維持やスケーリング設計を自前で抱える必要がありません。
  • Azure エコシステムとの親和性:
    汎用クラウドブラウザサービス(BrowserStack など)と比べて、Entra IDによる認証やRBACによる権限管理、NSGやファイアウォールを前提にしたネットワーク制御を、既存のAzure運用に乗せやすい点が特徴です。
  • 柔軟なネットワーク制御:
    リージョンごとの送信元 IP 範囲が公開されているため、必要に応じてNSGやファイアウォールの許可ルールに盛り込むことができます。
    また、プライベートな環境やlocalhost向けのテストについては、exposeNetwork設定することで、「Playwrightを実行している側から見えるネットワーク」をクラウドブラウザに接続することができます。

4-3. 既存の Playwrightテストの移行方法

既存のテストを Playwright Workspacesへ載せる手順は非常にシンプルです。テストを作り直すのではなく、「ブラウザの実行先をローカルからクラウドへ切り替える」というイメージで移行できます。

  1. Azure ポータルでワークスペースを作成する
  2. Playwright Workspaces専用パッケージ@azure/playwrightをインストールする
  3. 環境変数PLAYWRIGHT_SERVICE_URLにブラウザエンドポイントを設定する
  4. 認証方法(Entra ID等)を設定
  5. npx playwright test --config=playwright.service.config.tsコマンドでテストを実行する

参考)クイック スタート: Playwright Workspacesを使用してエンド ツー エンドのテストを大規模に実行する

4-4. CI/CDへの組み込み

ここまでで、既存のPlaywrightテストをPlaywright Workspaces上で稼働させるための基本設定はほぼ完了しました。実運用に向けて次に検討すべきは、CI/CDの中でどのような設定で継続的に運用するかです。

4-4-1. 成功/失敗の判定とレポートの活用

CI/CDでPlaywrightテストの成功/失敗を判定する基本は、テスト実行コマンドの「終了ステータス」を参照することです。コマンドが正常終了してCIジョブが成功で終われば「テスト成功」、ジョブが異常終了で終われば「テスト失敗」と判定できます。

実運用では、単純な成功 / 失敗の二値判定に加えて、「再試行(retry)の結果としてfailedが残ったか」や「Flakyなテストをどう扱うか」というポリシーを決めておくと、運用の安定性が向上します。

  • リトライの活用:
    CI環境ではretriesを有効にし、最終的に失敗が残った場合のみジョブを失敗させる運用が一般的です。
  • 厳格な品質管理:
    不安定なテスト(Flaky Tests)も厳しく扱いたい場合は、--fail-on-flaky-testsオプションを使用することで、検知レベルを引き上げることが可能です。

また、Playwright Workspacesのレポート機能を有効にすることで、テスト結果や関連アーティファクトを Azure Storageに自動保存し、Azureポータル上からレポートを確認できるようになります。CIのログのみに依存せず、失敗したテストや関連アーティファクトをレポート上から追跡できるようにしておくことで、継続運用におけるデバッグ効率が大幅に向上します。

4-4-2. 並列実行数の設定

Playwright Workspacesではクラウド側の計算資源を活用したテストの並列実行が可能です。一般的にworker数(=並列数)を増やすと、テストの完了時間は短縮されます。

また、テストを並列実行する場合、シナリオごとの結果出力が前後し、どのテストがどの結果に対応しているのかを把握しづらくなることがありますが、この点においても前節で紹介したレポート機能は有効です。並列実行されたテストであっても、各テストの結果は分散せず、1 回のテスト実行に対応する単一のレポートとしてまとめて出力されるため、Azure ポータル上でテストケース単位で結果を確認できます。

ただし、並列数を増やせば、無制限・無条件に高速化されるわけではありません。テスト全体の所要時間には、クライアント側のリソース状況、ネットワーク待機時間、テストコードの複雑さ、Playwrightの構成設定など、複数の要素が影響します。

そのため、まずは少なめの並列数から稼働させ、完了時間、実行の安定性、レポート上での結果確認のしやすさを観測しながら、自プロジェクトにとって最適な並列数を見極めていく調整方法が現実的です。

4-4-3. 認証方式

認証方式については、セキュリティと運用性の両面から、Microsoft Entra IDの使用が推奨されます。

Playwright Workspacesでは Entra ID認証が既定となっており、アクセストークン認証は明示的に有効化しない限り利用できません。アクセストークンは長寿命のパスワードに近い扱いになりやすく、保護やローテーションを自前で管理する負担が発生します。加えて、Workspacesのレポート機能が Entra ID認証の場合のみ利用可能であることからも、継続運用を見据えれば Entra IDを選択するのが最善です。

Entra IDを使った認証の中でも、フェデレーション資格情報を利用する方式は、CI環境に長期シークレットを持ち込まずに済むため、有力な選択肢となります。
一方で、ここでは、より汎用的な(環境を問わず採用しやすい)方法として、クライアントシークレットを用いたサービス プリンシパル認証を解説します。Azure CLIを通じて一般的な形で利用できるため、まずはこの構成を押さえておくとスムーズです。

① アプリケーション(サービスプリンシパル)を作成する
※後の手順で使うため、「アプリケーション(クライアント)ID」と「ディレクトリ(テナント)ID」を控えておきます。

② クライアントシークレットを作成する
※クライアントシークレットの値は作成直後にのみ表示されます。必ずその場で安全な場所に記録・保管してください。

③ 作成したアプリケーションにロールを割り当てる
Playwright Workspacesを利用するには、以下の2つの権限設定が必要です。

                
ロール スコープ目的
共同作成者 Playwright ワークスペース テストの実行
ストレージ BLOB データ共同作成者 レポート出力先のストレージアカウント レポートの出力

※Azureポータル上でレポートを閲覧するには、閲覧するユーザー自身にも、レポート出力先のストレージアカウントのIAMで「共同作成者」ロールが割り当てられている必要があります。

④ ログインし、テストを開始する
CIジョブの冒頭で以下のコマンドを実行し、認証を完了させます。これにより、その後のテスト実行が可能になります。

az login --service-principal \
    --username $APP_ID \
    --tenant $TENANT_ID \
    --password $CLIENT_SECRET

5. おわりに

本記事では、Playwrightテストを継続的に運用していくために、ベストプラクティスに基づくテスト設計、生成AIを活用した改善プロセス、そして Azure実行基盤の構築という3つの観点から解説しました。

振り返ると、これらに共通するのは、「属人的な努力ではなく、仕組みとして保守性を担保する」という考え方です。fixture / setupによる前提条件の管理、生成AIを活用したコード構造の改善、CI/CDに組み込まれた継続的な自動実行はいずれも、特定の担当者のスキルや注意力に頼るのではなく、システムとして持続可能な形でテスト品質を維持するための設計判断だと言えます。

また、本記事で紹介した生成AIの活用は、あくまで執筆時点(2026年5月)でのアプローチです。
Playwright自体も、Test Agents をはじめ、AIがテストシナリオの構成や修復を支援する方向へ進化を続けています。今後は、ベストプラクティスへの準拠チェックやリグレッション検知といった領域でも、AI を活用した支援がさらに広がっていくと考えられます。

そうした変化の中でも、本記事で整理した「何を検証すべきか」「どこまでを自動化し、どこを人間が判断するか」という設計指針は、ツールや技術が変わっても重要であり続けるはずです。
E2Eテストは、自動化すること自体が目的ではありません。リリースや仕様変更のたびに確実なフィードバックを返し、システムの品質を担保する「実効性のある仕組み」として定着してこそ、本来の役割を果たせます。

本記事が、Playwrightテストを単なる一時的な成果物で終わらせず、長期的にシステムの品質を支え続ける「価値ある資産」へと育て上げるための一助になれば幸いです。