💫

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

2024/09/26に公開

背景

前回の記事で、Next.js(App Router)、Zod、Prisma、React Hook Formを使用してTODOリストアプリケーションを作成しました。

今回は、このアプリケーションにvitestを使用してテストを追加する方法を紹介します。

GitHubリポジトリはこちらです。

Vitestとは

vitestは、Viteベースの高速なJavaScriptテストフレームワークです。Jest互換のAPIを持ち、TypeScriptのサポートも優れています。

1. 必要なパッケージのインストール

yarn add -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/jest-dom
yarn add testcontainers @testcontainers/postgresql

2. 設定ファイル

vite.config.tsの作成

vite.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "path";

export default defineConfig({
  plugins: [react()],
  test: {
    environment: "jsdom",
    globals: true,
    setupFiles: ["./vitest.setup.ts"],
    include: ["src/**/*.test.ts"],
  },
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
});

tsconfig.jsonの更新

tsconfig.json
{
  "compilerOptions": {
    "types": ["node", "vitest/globals", "testcontainers"]
  },
  "include": [
    "tests/**/*.ts"
  ]
}

3. テスト用データベースのセットアップ

テスト用のデータベースをセットアップするために、testcontainersを使用します。これにより、テストごとに隔離された環境を簡単に作成できます。また、setTestPrisma関数を使用して、マイグレーション済みのPrismaインスタンスを維持します。

testcontainersを使ったデータベースセットアップ

vitest.setup.tsファイルを作成し、以下のコードを追加します:

vitest.setup.ts
import "@testing-library/jest-dom";
import { beforeAll, afterAll } from "vitest";
import { PrismaClient } from "@prisma/client";
import { execSync } from "child_process";
import {
  PostgreSqlContainer,
  StartedPostgreSqlContainer,
} from "@testcontainers/postgresql";
import { setTestPrisma } from "./prisma";

let container: StartedPostgreSqlContainer;
let prisma: PrismaClient;

export async function setupTestDBContainer() {
  // testcontainersを使用してPostgreSQLコンテナを起動
  container = await new PostgreSqlContainer("public.ecr.aws/docker/library/postgres:16.1-alpine")
    .withDatabase("todo_db")
    .withUsername("todouser")
    .withPassword("todopassword")
    .withReuse()
    .start();

  const port = container.getMappedPort(5432);
  
  // データベースURLを環境変数に設定
  process.env["DATABASE_URL"] = `postgresql://${container.getUsername()}:${container.getPassword()}@${container.getHost()}:${port}/todo_db?schema=public`;

  // Prismaマイグレーションを実行
  execSync("npx prisma migrate deploy", { stdio: "inherit" });

  // 新しいPrismaインスタンスを作成
  prisma = new PrismaClient({
    datasources: {
      db: {
        url: process.env.DATABASE_URL,
      },
    },
  });

  // マイグレーション済みのPrismaインスタンスをセット
  setTestPrisma(prisma);
}

beforeAll(async () => {
  console.log("Start Global setup");
  await setupTestDBContainer();
  console.log("End Global setup");
});

afterAll(async () => {
  console.log("Start Global teardown");
  await prisma.$disconnect();
  await container.stop();
  console.log("End Global teardown");
});

setTestPrismaの実装

prisma.tsファイルに以下のコードを追加して、setTestPrisma関数を実装します:

prisma.ts
import { PrismaClient } from "@prisma/client";

let prisma: PrismaClient;

export function getPrismaClient(): PrismaClient {
  if (!prisma) {
    prisma = new PrismaClient();
  }
  return prisma;
}

export function setTestPrisma(newPrisma: PrismaClient) {
  prisma = newPrisma;
}

なぜこのアプローチが重要か

1. 隔離された環境

testcontainersを使用することで、各テスト実行時に独立したデータベース環境を確保できます。これにより、テスト間の干渉を防ぎ、より信頼性の高いテスト結果を得ることができます。

2. 再現性

コンテナ化されたデータベースを使用することで、ローカル環境やCI/CD環境で一貫したテスト環境を維持できます。

3. マイグレーション済みPrismaインスタンスの維持

setTestPrisma関数を使用することで、マイグレーション後のPrismaインスタンスを全てのテストで共有できます。これにより、各テストでデータベーススキーマが正確に反映されていることを保証できます。

4. パフォーマンス

テスト実行ごとにデータベースをセットアップするのではなく、マイグレーション済みのインスタンスを再利用することで、テストの実行速度を向上させることができます。

このアプローチを採用することで、より堅牢で信頼性の高いテスト環境を構築でき、アプリケーションの品質向上に貢献します。

4. テストの実装

src/app/action.medium.test.tsファイルを作成し、TODOアクションのテストを実装します。

src/app/action.medium.test.ts
import { describe, expect, test } from "vitest";
import { addTodo, deleteTodo, toggleTodo } from "./actions";

describe("addTodo", () => {
  test("addTodoが正常に動作すること", async () => {
    const newTodo = { title: "New todo" };
    await expect(addTodo(newTodo)).resolves.toEqual({
      id: 1,
      title: "New todo",
    });
  });

  test("空のタイトルでは、addTodoが失敗すること", async () => {
    const newTodo = { title: "" };
    await expect(addTodo(newTodo)).rejects.toThrow();
  });
});

describe("toggleTodo", () => {
  test("toggleTodoが正常に動作すること", async () => {
    const newTodo = { title: "New todo" };
    const { id } = await addTodo(newTodo);
    await expect(deleteTodo(id)).resolves.toBeUndefined();
  });

  test("存在しないidでは、deleteTodoが失敗すること", async () => {
    await expect(deleteTodo(100)).rejects.toThrow();
  });
});

5. テストの実行

テストを実行するには、以下のコマンドを使用します:

yarn vitest

まとめ

この記事では、Next.js(App Router)で作成したTODOアプリにvitestを使用してテストを追加する方法を紹介しました。主な学びポイントは以下の通りです:

  • vitestの基本的な設定方法
  • テスト用データベースのセットアップ
  • サーバーアクションのテスト実装

このアプローチを採用することで、アプリケーションの信頼性を向上させ、バグの早期発見に役立ちます。

Discussion