Next.js(App Router)で作ったTODOアプリにPlaywrightでテストを追加しました
背景
前回の記事で、Next.js(App Router)、Zod、Prisma、React Hook Formを使用してTODOリストアプリケーションを作成しました。
今回は、このアプリケーションにPlaywrightを使用してエンドツーエンド(E2E)テストを追加する方法を紹介します。
GitHubリポジトリはこちらです。
Playwrightとは
Playwrightは、Microsoftが開発したモダンなWeb測試自動化フレームワークです。複数のブラウザ(Chromium、Firefox、WebKit)をサポートし、高速で信頼性の高いテストを可能にします。自動待機機能や強力なセレクタ、ネットワークインターセプトなどの機能を備えており、E2Eテストの作成と実行を効率化します。
1. Playwrightのインストール
まず、Playwrightをプロジェクトにインストールします。
yarn create playwright
インストール時の質問には以下のように答えてください:
- TypeScriptを使用するか? → Yes
- テストフォルダの名前 → tests(またはお好みの名前)
- GitHub Actionsワークフローを追加するか? → No
- Playwrightブラウザをインストールするか? → Yes
2. 設定ファイルの修正
Playwrightを使用するために、いくつかの設定ファイルを修正する必要があります。
{
"name": "todo-list-next-and-react-hook-form",
"version": "0.1.0",
"private": true,
"type": "module", // 追加
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@prisma/client": "^5.17.0",
"next": "14.2.5",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.52.2",
"zod": "^3.23.8",
"@t3-oss/env-nextjs": "^0.7.1" // 追加
},
"devDependencies": {
"@playwright/test": "^1.47.0", // 追加
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.5",
"postcss": "^8",
"prisma": "^5.17.0",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "tests/todo.test.ts"], // "tests/todo.test.ts"の追加
"exclude": ["node_modules"]
}
3. テストの実装
Playwrightを使用して効果的なテストを書くために、テストコードを構造化し、再利用可能なコンポーネントを作成します。
ディレクトリ構造
tests/
├── src/
│ ├── components/
│ │ ├── button.ts
│ │ ├── input.ts
│ │ └── text.ts
│ ├── features/
│ │ └── todo.ts
│ ├── pages/
│ │ └── todo.ts
│ └── system/
│ └── database.ts
└── todo.test.ts
コンポーネントの抽象化
各UIコンポーネントを抽象化することで、再利用性とメンテナンス性が向上します。
例:
import { Locator, Page, expect } from "@playwright/test";
export class Button {
private readonly scope: Page | Locator;
private readonly label: string;
constructor({ scope, label }: { scope: Page | Locator; label: string }) {
this.scope = scope;
this.label = label;
}
// 要素
protected get button() {
return this.scope.getByRole("button", { name: this.label, exact: true });
}
// 動作
async クリックする(): Promise<void> {
await this.button.click();
}
// 状態
async あること(): Promise<void> {
await expect(this.button).toBeVisible();
}
}
同様に、InputやTextコンポーネントも抽象化します。
ページオブジェクトモデル
ページの構造と操作を抽象化することで、テストの可読性と保守性が向上します。
例:
import { Page } from "@playwright/test";
import { Input } from "../components/input.js";
import { Button } from "../components/button.js";
import { Text } from "../components/text.js";
export class Todoページ {
private readonly page: Page;
constructor(page: Page) {
this.page = page;
}
async 開く(): Promise<void> {
await this.page.goto("http://localhost:3001");
}
get todoフォーム(): Input {
return new Input({
scope: this.page,
label: "New todo",
});
}
get 追加ボタン(): Button {
return new Button({
scope: this.page,
label: "Add",
});
}
// ...
}
機能テスト
ビジネスロジックに基づいたテストを記述することで、アプリケーションの動作を明確に表現できます。
例:
import { Page } from "@playwright/test";
import { Todoページ } from "../pages/todo";
export class Todo機能 {
private readonly Todoページ: Todoページ;
constructor(private readonly page: Page) {
this.Todoページ = new Todoページ(page);
}
async 新たなTodoを追加する(title: string) {
await this.Todoページ.開く();
await this.Todoページ.todoフォーム.入力する(title);
await this.Todoページ.追加ボタン.クリックする();
await this.Todoページ.追加されたTodoリスト.次の値を含むこと(title);
}
async 既存のTodoを削除する() {
await this.Todoページ.開く();
await this.Todoページ.削除ボタン.クリックする();
await this.Todoページ.追加されたTodoリスト.ないこと();
}
}
メインのテストファイル
最終的に、これらの抽象化されたコンポーネントと機能を使用してテストを記述します。
例:
import { test } from "@playwright/test";
import { resetDatabase } from "./src/system/database.js";
import { Todo機能 } from "./src/features/todo";
test.beforeEach(async () => {
await resetDatabase();
});
test.describe("Todoリスト", () => {
test("新たなTODOを追加できる", async ({ page }) => {
await new Todo機能(page).新たなTodoを追加する("ご飯を食べる");
});
test("TODOを削除できる", async ({ page }) => {
await new Todo機能(page).新たなTodoを追加する("ご飯を食べる");
await new Todo機能(page).既存のTodoを削除する();
});
});
この構造化されたアプローチの利点:
再利用性: コンポーネントやページオブジェクトは複数のテストで再利用できます。
保守性: UIの変更があった場合、影響を受ける箇所が限定されます。
可読性: テストコードがより宣言的になり、ビジネスロジックが明確になります。
拡張性: 新しいテストケースの追加が容易になります。
4. テストの実行
テストを実行する前に、アプリケーションが起動していることを確認してください。別のターミナルで以下のコマンドを実行します:
yarn dev
その後、以下のコマンドでテストを実行します:
yarn playwright test
5. テスト結果の確認
テストが完了すると、コンソールに結果が表示されます。また、詳細なレポートを見るには以下のコマンドを使用します:
yarn playwright show-report
まとめ
この記事では、Playwrightを使用してNext.jsアプリケーションにE2Eテストを追加する方法を学びました。主な学びポイントは以下の通りです:
- Playwrightの基本的な使用方法
- テストコードの構造化と再利用可能なコンポーネントの作成
- ページオブジェクトモデルの利用
- 機能テストの実装
この構造化されたアプローチには以下の利点があります:
- 再利用性: コンポーネントやページオブジェクトは複数のテストで再利用できます。
- 保守性: UIの変更があった場合、影響を受ける箇所が限定されます。
- 可読性: テストコードがより宣言的になり、ビジネスロジックが明確になります。
- 拡張性: 新しいテストケースの追加が容易になります。
学んだこと
pageオブジェクト
pageオブジェクトは、ブラウザ内の単一のタブまたはウィンドウを表す。
テストの中で最も頻繁に使用するオブジェクトの1つ。
主な機能:
1. ナビゲーション:Webページへの移動
await page.goto('https://example.com');
2. 要素の操作:クリック、入力など
await page.click('button');
await page.fill('input[name="username"]', 'user');
3. 情報の取得:ページのタイトル、URL、コンテンツの取得
const title = await page.title();
const url = await page.url();
cont content = await page.content();
locatorオブジェクト
locatorオブジェクトは、ページ上の要素を特定する方法を提供する。
page.locator()メソッドを使用して作成する。
主な特徴:
1. 自動待機
要素が利用可能になるまで自動的に待機する。
2. 複数の要素
複数の要素にマッチする場合、全ての要素に対して操作を行う。
3. チェーン可能
複数のlocatorを組み合わせて、より具体的な要素を特定できる。
使用例:
// テキストで要素を特定
const addButton = page.locator('text=Add');
// CSSセレクタで特定
const todoItem = page.locator('.todo-item');
// 複数の条件を組み合わせる
const completedTodo = page.locator('.todo-item').filter({hasText: 'Completed'});
// 操作の実行
await addButton.click();
await todoItem.first().check();
await expect(completedTodo).toBeVisible();
Discussion