💫

Next.js(App Router)で作ったTODOアプリにPlaywrightでテストを追加しました

2024/09/26に公開

背景

前回の記事で、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を使用するために、いくつかの設定ファイルを修正する必要があります。

package.json
{
  "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"
  }
}
tsconfig.json
{
  "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コンポーネントを抽象化することで、再利用性とメンテナンス性が向上します。
例:

tests/src/components/button.ts
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コンポーネントも抽象化します。

ページオブジェクトモデル

ページの構造と操作を抽象化することで、テストの可読性と保守性が向上します。
例:

tests/src/pages/todo.ts
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",
    });
  }

  // ... 
}

機能テスト

ビジネスロジックに基づいたテストを記述することで、アプリケーションの動作を明確に表現できます。

例:

tests/src/features/todo.ts
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リスト.ないこと();
  }
}

メインのテストファイル

最終的に、これらの抽象化されたコンポーネントと機能を使用してテストを記述します。

例:

tests/todo.test.ts
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テストを追加する方法を学びました。主な学びポイントは以下の通りです:

  1. Playwrightの基本的な使用方法
  2. テストコードの構造化と再利用可能なコンポーネントの作成
  3. ページオブジェクトモデルの利用
  4. 機能テストの実装

この構造化されたアプローチには以下の利点があります:

  • 再利用性: コンポーネントやページオブジェクトは複数のテストで再利用できます。
  • 保守性: 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