1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

背景

前回の記事で、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();
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?