フロントエンドの E2E テストを書いてみた

はじめに

 フロントエンドの実装コードが複雑化している中で、フロントエンドでのテストコードはとても重要視されていると思います。そんなフロントエンドのテスト種別の中の E2E テストについて、どのような目的で導入され、どのようなメリットがあるのか知りたかったため今回調査を行いました。E2E テストにあまり触れたことのないフロントエンドエンジニアの方に向けて、その内容を共有できればと思います。

E2E テストとは?

 E2E ( End to End ) テストは 、一言で表すなら「実際のブラウザ上でユーザーと同じように操作し行うテスト」のことです。
 フロントエンドにおける E2E テストは、E2E テスト用のツールを導入し、E2E テストを自動化させる文脈で使われます。自動化のざっくりとしたイメージとしては、テストが実行されると実際にブラウザが立ち上がり、行なってほしいテスト操作が自動で行なわれます。そしてエラーが起きていないか、テストケースをクリアしているかどうかを判定してくれます。
 E2E テストは次の場面で活用されます。

  • フロントエンドからバックエンド(DB を含む)まで繋げた状態でのテスト
  • 実際のブラウザ上でしか検証できないような機能のテスト

 本番環境に近い環境で、ユーザーと同じように実際のブラウザを操作しテストを行うことで、実環境上でのみ現れる不具合を検知し、品質を担保することができます。しかし、E2E テストの使用にはメリット・デメリットが存在するため、テストを導入する場所は精査する必要があります。
 テスティングトロフィーにもあるように、ユーザーが操作する環境に近い状態でテストを行うので、信頼性は高いのですが、コードの保守やテストの実行時間などのコストが最も高いものとなっています。そのため、プロダクトとしてエラーが発生すると成り立たないような、コアな機能などへ導入するのが良さそうです。

E2E テストを行うメリット・デメリット

 E2E テストのメリットとしては、ユーザーが操作する環境に近い状態でテストするため、結合テストや単体テストではカバーできない範囲の検証を行うことができます。これにより、本番環境で遭遇するようなバグを検知し、アプリケーションの品質を向上することができます。
 一方、デメリットとしては、システム全体を通してテストを行うため、テストの実行時間が増加したり、通信を伴うテストケースにおいてはネットワーク環境などが影響し、テストが通らない場合もあります。

試しに書いてみた

 E2E テストに使用されるツールとして CypressPuppeteer などがありますが、今回は、最近勢いのあるマイクロソフト製の Playwright で試してみました!
動作環境としては以下のとおりです。

  • vite 5.0.8
  • react 18.2.0
  • playwright/test 1.41.1

テストの内容は、以下の2つです。

  • ログイン
    • 入力フォームにログイン情報を入力→クリック→ホーム画面に遷移できること
  • メディアクエリー
    • デバイスの横幅が 756 px 未満の時はヘッダーのリストメニューがハンバーガーメニューになること

それぞれの機能に対して以下のとおりテストコードを書いてみました。

ログインのテストコード
import { test, expect } from "@playwright/test";

test.describe("ログインのテスト", () => {
  // テストを実行する前に行いたい処理を記述
  test.beforeEach(async ({ page }) => {
    await page.goto("http://localhost:5173"); // 処理 1
  });

  test("ログイン成功しホーム画面へ遷移する", async ({ page }) => {
    await page.getByLabel("Eメール").fill("test@example.com"); // 処理 2
    await page.getByLabel("パスワード").fill("password"); // 処理 3

    await page.getByRole("button", { name: "ログイン" }).click(); // 処理 4

    await expect(
      page.getByRole("heading").filter({ hasText: "ようこそ" })
    ).toBeVisible(); // 処理 5
  });

  test("ログインに失敗しログイン失敗用の文言が表示されている", async ({
    page,
  }) => {
    await page.getByLabel("Eメール").fill("test@example.com"); // 処理 2
    await page.getByLabel("パスワード").fill("error_password"); // 処理 3

    await page.getByRole("button", { name: "ログイン" }).click(); // 処理 4

    await expect(
      page
        .getByRole("alert")
        .filter({ hasText: "Eメールまたはパスワードが間違っています。" })
    ).toBeVisible(); // 処理 5
  });
});

テストコードの流れを説明します。

  1. page.goto でテスト対象のページへアクセス
    1. 今回はローカル環境で実行するため vite で立ち上げている url を指定
  2. getByLabel で E メールの要素を取得し、fill でテストアカウントのメールアドレスを入力する
  3. getByLabel でパスワードの要素を取得し、fill でテストアカウントのパスワードを入力する
  4. ログイン処理を実行させるために、getByRole を用いてログインボタンの要素を取得し、ボタン押下を行う(ここでホーム画面への遷移が発生)
  5. ようこそという文言が表示されていることを確認
    1. エラー系の場合は遷移しないことを確認するために、Eメールまたはパスワードが間違っています。文言が表示されていることを確認
メディアクエリのテストコード
import { test, expect } from "@playwright/test";

// テストを実行する前に行いたい処理を記述
test.beforeEach(async ({ page }) => {
  await page.goto("http://localhost:5173"); // 処理 1
});

test.describe("画面幅が 755px 以下の時のテスト", () => {
  test.use({
    viewport: {
      width: 755,
      height: 880,
    },
  }); // 処理 2

  test("ハンバーガーメニューが表示される", async ({ page }) => {
    await expect(
      page.getByRole("button", { name: "ハンバーガーメニューを開く" })
    ).toBeVisible(); // 処理 3
  });
});

test.describe("画面幅が 756px 以上の時のテスト", () => {
  test.use({
    viewport: {
      width: 756,
      height: 880,
    },
  }); // 処理 2

  test("リストメニューが表示される", async ({ page }) => {
    await expect(page.getByRole("list")).toBeVisible(); // 処理 3
  });
});

テストコードの流れを説明します。

  1. page.goto でテスト対象のページへアクセス
  2. テストを実行するブラウザの viewport を use 関数を使用して設定
  3. 756px 未満の場合はハンバーガーメニューが表示されていることを確認
    1. 756px 以上の場合は、リストメニューが表示されていることを確認

終わりに

 今回は E2E テストの概要の調査から基本的なテストコードを書き、ローカルでテストを実行しました。次のステップとして、CI への組み込みを行っていきたいです!
 Playwright にはまだまだ機能がたくさんあるので、公式ドキュメントを参考にしながら複雑なコードに対するテストも記述できるように勉強していきます!

執筆:西田・谷村

参考文献