Next.js(App Router)で作ったTODOアプリにVitestでテストを追加しました
背景
前回の記事で、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の作成
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の更新
{
"compilerOptions": {
"types": ["node", "vitest/globals", "testcontainers"]
},
"include": [
"tests/**/*.ts"
]
}
3. テスト用データベースのセットアップ
テスト用のデータベースをセットアップするために、testcontainersを使用します。これにより、テストごとに隔離された環境を簡単に作成できます。また、setTestPrisma関数を使用して、マイグレーション済みのPrismaインスタンスを維持します。
testcontainersを使ったデータベースセットアップ
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関数を実装します:
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アクションのテストを実装します。
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