PlaywrightでE2Eテスト書いてみた
Playwrightとはなんぞや?
Playwright(毎回Playwriteって書きそうになる)はフロントエンド向けのE2Eテストフレームワークです。
E2Eテストはアプリケーションが期待通り動作してデータフローが適切に機能しているかどうかを確認するテストです。要はユーザー視点でアプリケーションを操作して期待通りに動作するか検証するので、範囲が膨大になることが多くアプリケーションによっては非常に時間とコストがかかるテストです。
公式のドキュメントには以下のように書いてあります。
Playwright Testは、最新のWebアプリ向けのエンドツーエンドのテストフレームワークです。テストランナー、アサーション、分離、並列化、そして豊富なツール群を備えています。Playwrightは、Windows、Linux、macOS上のChromium、WebKit、Firefoxをサポートし、ローカル環境でもCI環境でも、ヘッドレス環境でもヘッドレス環境でも実行可能です。Chrome(Android)とMobile Safariのネイティブモバイルエミュレーションも利用可能です。
様々なレンダリングエンジンやモバイルエミュレーションをサポートしているのでブラウザや検証機材を変える必要がなくなります。ありがたいですね!
事前準備
今回はRemixアプリで動作確認していこうと思います。
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の方をインストールしていきます。
ドキュメントの手順に沿って進めていきます。
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にありますね。
実行ボタンを押すと書いたテストケースがパスされました
もうちょい詳しく
テストケースは独立している
テストケースを複数作成する場合それぞれのテストケースでページは分離している、つまりテストケースごとにページは常に最新の状態になります。
例えば
- 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
ブラウザ操作を録画してテストコードに起こしてくれるようなイメージ。
例えば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に操作させるものもあるのでそちらを使った方が良いと思います。
参考
Discussion