🦔

PlaywrightでE2Eテスト書いてみた

に公開

Playwrightとはなんぞや?

Playwright(毎回Playwriteって書きそうになる)はフロントエンド向けのE2Eテストフレームワークです。

E2Eテストはアプリケーションが期待通り動作してデータフローが適切に機能しているかどうかを確認するテストです。要はユーザー視点でアプリケーションを操作して期待通りに動作するか検証するので、範囲が膨大になることが多くアプリケーションによっては非常に時間とコストがかかるテストです。

公式のドキュメントには以下のように書いてあります。

Playwright Testは、最新のWebアプリ向けのエンドツーエンドのテストフレームワークです。テストランナー、アサーション、分離、並列化、そして豊富なツール群を備えています。Playwrightは、Windows、Linux、macOS上のChromium、WebKit、Firefoxをサポートし、ローカル環境でもCI環境でも、ヘッドレス環境でもヘッドレス環境でも実行可能です。Chrome(Android)とMobile Safariのネイティブモバイルエミュレーションも利用可能です。

様々なレンダリングエンジンやモバイルエミュレーションをサポートしているのでブラウザや検証機材を変える必要がなくなります。ありがたいですね!

事前準備

今回はRemixアプリで動作確認していこうと思います。

https://v2.remix.run/docs/start/quickstart

Remixのプロジェクトを作成したらapp/routes/_index.tsxを作成します。

// app/routes/_index.tsx
import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router";
import { data, redirect } from "react-router";
import { Form, useLoaderData } from "react-router";

let likeCount = 0; // 学習用の簡易メモリ保持(本番はDBなどへ)

export async function loader(_args: LoaderFunctionArgs) {
  return data({ likes: likeCount });
}

export async function action({ request }: ActionFunctionArgs) {
  const form = await request.formData();
  if (form.get("intent") === "increment") {
    likeCount += 1;
  }
  return redirect("/"); // POST-Redirect-GET
}

export default function Index() {
  const { likes } = useLoaderData<typeof loader>();
  return (
    <main style={{ padding: 24 }}>
      <h1>いいねアプリ</h1>
      <p data-testid="like-count">いいね数: {likes}</p>
      <Form method="post">
        <button
          type="submit"
          name="intent"
          value="increment"
          style={{ fontSize: 24 }}
        >
          いいね +1
        </button>
      </Form>
    </main>
  );
}

Playwrightをインストール

ではPlaywrightの方をインストールしていきます。
ドキュメントの手順に沿って進めていきます。

https://playwright.dev/docs/intro

npm init playwright@latest

> npx
> create-playwright

Getting started with writing end-to-end tests with Playwright:
Initializing project in '.'
✔ Where to put your end-to-end tests? · tests
✔ Add a GitHub Actions workflow? (Y/n) · true
✔ Install Playwright browsers (can be done manually via 'npx playwright install')? (Y/n) · true
Installing Playwright Test (npm install --save-dev @playwright/test)

インストールが完了するとtestsディレクトリ配下にexample.spec.tsが作成されます
ちなみにファイル名にspec.tsが入っていればテストコードとして認識されるみたいです。

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

test('has title', async ({ page }) => {
  await page.goto('https://playwright.dev/');

  // Expect a title "to contain" a substring.
  await expect(page).toHaveTitle(/Playwright/);
});

test('get started link', async ({ page }) => {
  await page.goto('https://playwright.dev/');

  // Click the get started link.
  await page.getByRole('link', { name: 'Get started' }).click();

  // Expects page to have a heading with the name of Installation.
  await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});

また.githubディレクトリにworkflowが自動で生成されるのでCIでテストを走らせることもできます。

name: Playwright Tests
on:
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: lts/*
    - name: Install dependencies
      run: npm ci
    - name: Install Playwright Browsers
      run: npx playwright install --with-deps
    - name: Run Playwright tests
      run: npx playwright test
    - uses: actions/upload-artifact@v4
      if: ${{ !cancelled() }}
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30

E2Eのテストコードを書いていく

今回作成したアプリのテストコードを書いてみます。

// likebutton.spec.ts
import { test, expect } from "@playwright/test";

test("いいね+1ボタンを押すと、いいね数が+1される", async ({ page }) => {
  await page.goto("http://localhost:5173/"); // 立ち上げたRemixアプリのURL

  const likeCount = page.getByTestId("like-count");
  await expect(likeCount).toHaveText("いいね数: 0");

  const likeButton = page.getByRole("button", { name: "いいね +1" });
  await likeButton.click();

  await expect(likeCount).toHaveText("いいね数: 1");
});

まずpage.getByTestId("like-count")でいいねを表示する箇所のDOMを取得して期待値(いいね数: 0)通りの結果になっているかをアサーションします。

const likeCount = page.getByTestId("like-count");
await expect(likeCount).toHaveText("いいね数: 0");

次にpage.getByRole("button", { name: "いいね +1" })でいいねボタンのDOMを取得し、ボタンをクリック操作します。

const likeButton = page.getByRole("button", { name: "いいね +1" });
await likeButton.click();

またここではクリックまでの操作を2行で書いていますが、1行で書くこともできます。こちらの方がコンパクトに書けていいですね。

await page.getByRole("button", { name: "いいね +1" }).click();

最後にいいね数が増えているかどうかをアサーションします。

await expect(likeCount).toHaveText("いいね数: 1");

テストを実行してみる

ではテストを実行してみましょう。

npx playwright test --ui

コマンドを叩くとGUIが起動します。
先ほど作成したテストケースがTESTSにありますね。

image.png

実行ボタンを押すと書いたテストケースがパスされました
image.png

もうちょい詳しく

テストケースは独立している

テストケースを複数作成する場合それぞれのテストケースでページは分離している、つまりテストケースごとにページは常に最新の状態になります。

例えば

  • 1回クリックする
  • 2回クリックする

の二つのテストケースがある場合、2回クリックするテストケースで1回クリックするテストケースのページ状態を考慮したテストケースにする必要がないわけです。

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

test("いいね+1ボタンを押すと、いいね数が+1される", async ({ page }) => {
  await page.goto("http://localhost:5173/");

  // 初期状態のいいね数を確認
  // 初回アクセス時は0のはず
  const likeCount = page.getByTestId("like-count");
  await expect(likeCount).toHaveText("いいね数: 0");

  // いいね+1ボタンをクリック
  await page.getByRole("button", { name: "いいね +1" }).click();

  // クリック後、いいね数が+1されていることを確認
  await expect(likeCount).toHaveText("いいね数: 1");
});

test("いいね+1ボタンを2回押すと、いいね数が+2される", async ({ page }) => {
  await page.goto("http://localhost:5173/");

  // 初期状態のいいね数を確認
  // 初回アクセス時は0のはず
  const likeCount = page.getByTestId("like-count");
  await expect(likeCount).toHaveText("いいね数: 0");

  // いいね+1ボタンを2回クリック
  await page.getByRole("button", { name: "いいね +1" }).click();
  await page.getByRole("button", { name: "いいね +1" }).click();

  // クリック後、いいね数が+2されていることを確認
  await expect(likeCount).toHaveText("いいね数: 2");
});

ブラウザを操作してテストを生成することもできる

実際にブラウザをぽちぽち操作しながらテストコードを生成することもできる。
コマンドを叩くとUIが起動する。

npx playwright codegen http://localhost:5173

image.png

https://playwright.dev/docs/codegen-intro

ブラウザ操作を録画してテストコードに起こしてくれるようなイメージ。
例えばrecordした状態でいいねボタンを2回クリックしてみるとこのようなテストコードが生成される

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

test('test', async ({ page }) => {
  await page.goto('http://localhost:5173/');
  await page.getByRole('button', { name: 'いいね +' }).click();
  await page.getByRole('button', { name: 'いいね +' }).click();
  await page.locator('html').click();
});

使いこなせていないだけかもしれないが、使わないかも。慣れたらテストコードを書いた方が早い気がする。
また今はPlaywright MCPのようにブラウザをAIに操作させるものもあるのでそちらを使った方が良いと思います。

参考

https://playwright.dev/docs/intro

Discussion