💡

📝Playwright-mcp を使ったE2Eテストスクリプトの作成を試してみた

に公開

こんにちは!アルダグラムでQAエンジニアをしている千葉です!

最近AIの進化が目覚ましく、画像生成とかコードを書くAIとか何にでもAI使えるようになってきて、技術の進歩が凄まじい勢いだなって感じている今日この頃です。

突然ですが、私が担当している業務として、E2Eテストの自動化があります。

弊社でのE2Eテストの自動化は、MagicPodPlaywrightを利用しており、併用してテストの自動化を進めている状況です。

開発のメンバーでは、様々なAIを駆使して開発業務の効率化を行っていますが、QAメンバーでもAIを利用してテスト自動化の工数を削減する取り組みを行っていきたいと考えています。

今回は、Playwright-mcpCursorを使って、E2Eテストを自動で生成する方法の検証を行ったので、こちらをご紹介したいと思います!

1.Playwrightとは?

まずはPlaywrightについてです。

Playwrightは、Microsoftが開発したブラウザ自動化ツールで、ウェブアプリケーションのE2Eテスト(End-to-Endテスト)を簡単に行えるようにしてくれるツールです。

ChromeFirefoxWebKit(Safariのエンジン)をサポートしているので、クロスブラウザテストを一度にこなせるのが特徴です。

https://playwright.dev/docs/intro

2.Playwright-mcpについて

Playwright-mcpとは

今回の本題となる、Playwright-mcpについてです。

Playwright-mcpは、Playwrightの「更なる自動化」を目指すツールでして、AIを使ってPlaywrightテストスクリプトを自動で生成してくれるものです。

Playwrightはテストの自動化自体を行うツールですが、Playwright-mcpはPlaywrightのコードを自動で作成してくれるツールといった感じです。

主な特徴

公式からの引用ですが、こういった特徴があります。

  • 高速かつ軽量: ピクセルベースの入力ではなく、Playwright のアクセシビリティ ツリーを使用します。
  • LLM 対応: ビジョン モデルは必要なく、純粋に構造化データ上で動作します。
  • 決定論的なツールの適用: スクリーンショットベースのアプローチでよくある曖昧さを回避します。

ユースケース

こちらも公式からの引用です。
今回ご紹介するケースだとLLM手動の自動テストというところに当てはまります。

  • ウェブナビゲーションとフォーム入力
  • 構造化コンテンツからのデータ抽出
  • LLM主導の自動テスト
  • エージェント向け汎用ブラウザインタラクション

https://github.com/microsoft/playwright-mcp

3.Cursorについて

Cursorとは

Cursorについても、簡単にご紹介します。

Cursorは、Anysphereが開発したAI機能を搭載したコードエディタです。

Microsoftが提供しているVisual Studio Code(VS Code)をベースとしており、VSCodeライクな操作感で使用することができます。

Cursor自体はAIモデルを搭載してはおらず、外部のAIモデルと連携し、AI活用できるコーディング環境を提供しています。

料金体系

Cursorは無料プランと有料プランがあり、無料プランだとリクエスト数に制限がつきます。

プラン名 月額料金 主な機能・制限
Hobby 無料 ・Proプランの2週間無料トライアル
・月2,000回のコード補完
・月50回の低速プレミアムリクエスト
・ダウンロード機能
・プライバシーモード対応
Pro $20/月 ・Hobbyの全機能
・月500回の高速プレミアムリクエスト
・無制限の低速プレミアムリクエスト
・無制限のコード補完
・Cursor Tab機能(強力なオートコンプリート)
Business $40/ユーザー/月 ・Proの全機能
・組織全体でのプライバシーモード適用
・チームの一括請求
・利用状況を確認できる管理者向けダッシュボード
・SAML/OIDC SSO対応

https://www.cursor.com/ja

参考:次世代AIコードエディタ「Cursor」とは?できることや料金、使い方まで徹底解説

4.Playwright-mcp × CursorでE2Eテストの作成

ここからは、Playwright-mcpとCursorを連携する方法と、実際に簡単なテストシナリオの生成を行ってみたので、ご紹介します。

Playwright-mcpとCursorの設定

Playwright-mcpやCursorのインストール方法については、割愛します。

こちらでは、CursorでのPlaywright-mcpの設定方法について、ご紹介します。

  1. Cursorで右上の設定アイコンを押下し、Cursor Settingsを開きます。

  2. MCP > 「+ Add new global MCP server」ボタンを押下します。

  3. mcp.jsonに以下のコードを記載し保存します。

    {
      "mcpServers": {
        "playwright": {
          "command": "npx",
          "args": [
            "@playwright/mcp@latest"
          ]
        }
      }
    }
    
  4. 下の画像のように、playwrightの左横が緑色になれば連携完了です。

プロンプトでE2Eテスト作成

設定が完了したら、いよいよPlaywright-mcpを使ってE2Eテストを作成していきます!

💡 今回のE2Eテスト作成では、CursorはProプランを使用した状態を前提としています

今回は、弊社が提供しているKANNAのテスト環境で、簡単なテストケースをPlaywrightのコードとして起こしてみようと思います。

テストケース

  1. オーナー権限のアカウントでログイン
    1. ホーム画面でログインできたことをアサーションする
  2. 案件を作成する
    1. 案件が作成されたことをアサーション
  3. 案件情報を編集する
    1. 案件が更新されたことをアサーション

※ a でそれぞれ操作に対する期待結果をテストとして確認します

AIと対話(Page Object&テスト一括作成編)

1.テスト環境へ遷移

まずはテスト環境に遷移してみます。

Playwrightのテスト実行コマンドで—headedのオプションをつけて実行した時やPlaywright Test for VSCodeのPick Locatorを使用した場合と同様にブラウザが立ち上がりました。

2.テストケースをプロンプトとして渡す

ざっくりですが、以下のようなプロンプトを渡してみました。

これで、どれだけ理解してくれるのか…

【プロンプト】

以下のテストケースを実行し、test.spec.tsとして保存してください。

**テストケース**

1. オーナー権限のアカウントでログイン
    1. ホーム画面でログインできたことをアサーションする
2. 案件を作成する
    1. 案件が作成されたことをアサーション
3. 案件情報を編集する
    1. 案件が更新されたことをアサーション

ルール
・ページごとにPage Object Modelでページクラスを作成してください。(POMは既存のリポジトリを参考にしてください)
・1,2,3でそれぞれtest.stepを分けてください
・オーナー権限のアカウントはaccount.tsのownerAllOptionsのログイン情報を使用してください。
・ロケーターを取得する場合は、以下の優先順位に沿ったメソッドを使用してください。
 1.getByRole()
 2.そのほかのgetBy~()
 3.locator()
 4.その他

【回答】

リポジトリの構造を把握して、既存のPOMを参考にしてコード生成してくれてますね。

↓のようなspecファイルを作成してくれました。

test.spec.ts

import { test, expect } from '@playwright/test';
import { accounts } from '../data/account';
import { LoginPage } from '../pages/loginPage';
import { ProjectsPage } from '../pages/projectsPage';
import { ProjectCreatePage } from '../pages/projectCreatePage';
import { ProjectDetailPage } from '../pages/projectDetail/projectDetailPage';

test('オーナー権限での案件作成と編集テスト', async ({ page }) => {
  const loginPage = new LoginPage(page);
  const projectsPage = new ProjectsPage(page);
  const projectCreatePage = new ProjectCreatePage(page);

  // テストデータ
  const projectName = `テスト案件_${new Date().getTime()}`;
  const updatedProjectName = `更新_${projectName}`;

  await test.step('オーナー権限でログイン', async () => {
    await loginPage.goTo();
    await loginPage.logIn(accounts.ownerAllOptions.email, accounts.ownerAllOptions.password);

    // ホーム画面(案件一覧)に遷移したことを確認
    await expect(page).toHaveURL(/.*\/projects/);
    await expect(projectsPage.projectListTable).toBeVisible();
  });

  await test.step('案件を作成', async () => {
    // 案件作成ページに遷移
    await page.getByRole('button', { name: '案件を作成' }).click();

    // 案件情報を入力して作成
    await projectCreatePage.projectNameInput.fill(projectName);
    await projectCreatePage.sideCreateBtn.click();

    // 案件が作成されたことを確認
    await projectsPage.quickSearchProject(projectName);
    await expect(page.getByRole('rowheader', { name: projectName })).toBeVisible();
  });

  await test.step('案件情報を編集', async () => {
    // 案件詳細画面に遷移
    const projectDetailPage = await projectsPage.gotoProjectDetail(page, projectName);

    // 案件名を更新
    await projectDetailPage.projectNameEditLink.click();
    await projectDetailPage.projectNameInput.fill(updatedProjectName);
    await page.getByRole('button', { name: '保存' }).click();

    // 更新が反映されたことを確認
    await expect(projectDetailPage.headerProjectName).toHaveText(updatedProjectName);
  });
});

3.テストを実施してみる

【プロンプト】

${テスト環境}だけで作成したテストを実行してください

【回答】

テストが失敗しました。

どうやら、ログイン後の画面遷移先の期待値を案件一覧ページとしているようです。

実際はこのページは存在せず、ログイン後はトップページに遷移することを期待値としているので、期待結果を修正したプロンプトを投げてみます。

【プロンプト】

ログイン後は、案件一覧画面ではなくトップページに遷移することを期待値としてください。

【回答】

実際のトップページ上には存在しない「ダッシュボード」の文字列を持つロケーターを期待値としてしまいました。

ざっくりとしたプロンプトを渡すだけだと、完璧に動くテストスクリプトを一発で作成するのは難しいようです。

テスト失敗時のスクリーンショット

AIと対話(Page Object/テスト分割作成編)

先ほどまでは、テストケースをプロンプトとして渡して、一気にテストスクリプトを作成しようとしましたが、うまくいかなかったのでちょっとやり方を変えて小さめの粒度で検証してみます。

各ページに遷移して、そのPage Objectを作成してもらうようにしてみます。

1.テスト環境へ遷移

同じようにテスト環境に遷移します。

【プロンプト】

@https://${テスト環境のドメイン}/signin にアクセスしてください

【回答】

ログインページに遷移しました。
また、ページ内の要素についても言及してくれています。

2.Page Objectの作成

ページ内に表示されている要素を羅列してくれたので、ロケーターをまとめて取得させてみました。

【プロンプト】

このページのページオブジェクトを作成してください

【回答】

以下のようなPageクラスを作成してくれました。

ロケーターも実際の画面で使えるものが取得できていて、メソッドも比較的簡単なものですが汎用的に使えるものを用意してくれていますね。

import { Locator, Page } from '@playwright/test';

/** ログイン画面のページオブジェクト */
export class LoginPage {
  readonly page: Page;
  /** メールアドレス入力欄 */
  readonly emailField: Locator;
  /** パスワード入力欄 */
  readonly passwordField: Locator;
  /** ログインボタン */
  readonly loginBtn: Locator;
  /** パスワードを忘れた方リンク */
  readonly forgotPasswordLink: Locator;
  /** 外部アカウントでログインリンク */
  readonly externalLoginLink: Locator;
  /** 新規アカウント登録リンク */
  readonly signupLink: Locator;
  /** ログインでお困りの方リンク */
  readonly helpLink: Locator;
  /** 会社概要リンク */
  readonly companyInfoLink: Locator;
  /** お問い合わせリンク */
  readonly contactLink: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailField = page.getByPlaceholder('mail@example.com');
    this.passwordField = page.getByPlaceholder('8文字以上半角英数字');
    this.loginBtn = page.getByRole('button', { name: 'ログインする', exact: true });
    this.forgotPasswordLink = page.getByRole('link', { name: 'パスワードを忘れた方' });
    this.externalLoginLink = page.getByRole('link', { name: '外部アカウントでログイン' });
    this.signupLink = page.getByRole('link', { name: '新規アカウント登録(無料)' });
    this.helpLink = page.getByRole('link', { name: 'ログインでお困りの方' });
    this.companyInfoLink = page.getByRole('link', { name: '会社概要' });
    this.contactLink = page.getByRole('link', { name: 'お問い合わせ' });
  }

  /**
   * ログイン画面に遷移する
   */
  async goTo() {
    await this.page.goto('/signin');
  }

  /**
   * メールアドレスとパスワードを入力し、ログインする
   * @param email メールアドレス
   * @param password パスワード
   */
  async logIn(email: string, password: string) {
    await this.emailField.fill(email);
    await this.passwordField.fill(password);
    await this.loginBtn.click();
  }

  /**
   * パスワードリセットページに遷移する
   */
  async goToPasswordReset() {
    await this.forgotPasswordLink.click();
  }

  /**
   * 外部アカウントログインページに遷移する
   */
  async goToExternalLogin() {
    await this.externalLoginLink.click();
  }

  /**
   * 新規アカウント登録ページに遷移する
   */
  async goToSignup() {
    await this.signupLink.click();
  }

  /**
   * ヘルプページに遷移する
   */
  async goToHelp() {
    await this.helpLink.click();
  }
}

3.テストをケースをプロンプトとして渡す

先ほどより簡単なケースで、以下のようなシナリオを想定してプロンプトを渡してみました。

  1. ログインページに遷移
  2. 会社概要に遷移

【プロンプト】

会社概要に遷移するテストシナリオを作成して

【回答】

import { test, expect } from '@playwright/test';
import { LoginPage } from '../../pages/loginPage';

test.describe('ログインページからの会社概要遷移テスト', () => {
  let loginPage: LoginPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    await loginPage.goTo();
  });

  test('会社概要ページに正常に遷移できる', async ({ page, context }) => {
    // 新しいタブでの遷移を待ち受ける Promise を作成
    const popupPromise = page.waitForEvent('popup');

    // 会社概要リンクをクリック
    await loginPage.companyInfoLink.click();

    let targetPage: any;
    try {
      // 新しいタブが開かれるのを待つ(1秒でタイムアウト)
      targetPage = await popupPromise.then(
        (popup) => popup,
        () => page,
      );
    } catch {
      // タイムアウトした場合は現在のページを使用
      targetPage = page;
    }

    // ページの読み込みを待つ
    await targetPage.waitForLoadState();

    // URLが正しいことを確認
    expect(targetPage.url()).toBe('https://aldagram.com/');

    // ページタイトルを確認
    expect(await targetPage.title()).toBe('株式会社アルダグラム');

    // 会社の所在地情報が表示されていることを確認
    await expect(targetPage.getByText('東京都港区芝浦1丁目1-1')).toBeVisible();
    await expect(targetPage.getByText('浜松町ビルディング11階')).toBeVisible();
  });

  test('会社概要リンクが表示されている', async () => {
    // 会社概要リンクが表示されていることを確認
    await expect(loginPage.companyInfoLink).toBeVisible();

    // リンクのテキストが正しいことを確認
    await expect(loginPage.companyInfoLink).toHaveText('会社概要');
  });
});

4.テスト実施

【プロンプト】

テストを${テスト環境}だけで実行してください。
また、ブラウザを立ち上げたモードで実行してください

【結果】

以下のようなエラーが出ました。

どうやら新規タブで開かれることを期待値としていたようですが、実際は同タブで画面遷移するようですね。

Running 2 tests using 2 workers
[1/2] [xxxx] › tests/signIn/companyInfo.spec.ts:31:7 › ログインページからの会社概要遷移テスト › 会社
[2/2] [xxxx] › tests/signIn/companyInfo.spec.ts:12:7 › ログインページからの会社概要
  1) [xxxx] › tests/signIn/companyInfo.spec.ts:12:7 › ログインページからの会社概要遷移テスト › 会社概要ページに正常に遷移できる ───

    TimeoutError: page.waitForEvent: Timeout 10000ms exceeded while waiting for event "popup"
    =========================== logs ===========================
    waiting for event "popup"
    ============================================================

      15 |
      16 |     // 新しいタブで開かれることを確認
    > 17 |     const newPage = await page.waitForEvent('popup');
         |                                ^
      18 |     await newPage.waitForLoadState();
      19 |
      20 |     // URLが正しいことを確認

        at /Users/chibakoji/Desktop/work/kanna-playwright/tests/signIn/companyInfo.spec.ts:17:32

5.スクリプトの修正と再実行

先ほどのスクリプトではエラーになってしまったのですが、ここで終わらないのがPlaywright-mcpです。

自動的に、エラー内容を把握して自動でスクリプトを修正してくれました!

修正したテストケースを再実行し、今度は成功です!

だいぶ長い道のりでしたが、なんとか動くものをプロンプトだけで作成できました。

5.まとめ

検証結果

簡単に検証結果をまとめます。

  • Playwright-mcpを使うことで、Playwrightの知見がなくても、ある程度簡単なテストケースであればテストスクリプトを実装することができる
  • テストケースを一発で実装させることは難しい
  • ページクラスのロケーターの取得やメソッドの作成などの土台作成を、ある一定は任せることができる

結論

まだE2E自動テストスクリプト作成の補助的な領域をでないですが、初めてのテスト作成やページクラスの生成といった部分的な箇所では利用価値があると思います!

まだまだ、さまざまなプロンプトで検証する余地があると思いますし、Playwright-mcpも今後アップデートされていくかと思いますので、今後も引き続きAI活用したE2Eテスト自動化に取り組んでいきたいと思います!

もっとアルダグラムエンジニア組織を知りたい人、ぜひ下記の情報をチェックしてみてください!

アルダグラム Tech Blog

Discussion