概要
Route Handler 関数に new Request() を直接渡してレスポンスを検証するユニットテストパターン。E2E テストなしで GET / POST それぞれのハッピーパスとエラーパスを高速に確認できる。
インストール
npm install jest @types/jest ts-jest
実装
テスト対象の Route Handler
// app/api/users/route.ts
import { NextResponse } from "next/server";
type User = { id: number; name: string };
const users: User[] = [
{ id: 1, name: "山田太郎" },
{ id: 2, name: "田中花子" },
];
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const name = searchParams.get("name");
const result = name
? users.filter((u) => u.name.includes(name))
: users;
return NextResponse.json(result);
}
export async function POST(request: Request) {
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}
if (typeof (body as Record<string, unknown>).name !== "string") {
return NextResponse.json({ error: "name is required" }, { status: 400 });
}
const newUser: User = { id: users.length + 1, name: (body as { name: string }).name };
users.push(newUser);
return NextResponse.json(newUser, { status: 201 });
}
GET ハンドラのテスト
// app/api/users/route.test.ts
import { GET, POST } from "./route";
describe("GET /api/users", () => {
it("全ユーザーを返す", async () => {
const request = new Request("http://localhost/api/users");
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data).toHaveLength(2);
expect(data[0]).toMatchObject({ id: 1, name: "山田太郎" });
});
it("name クエリで絞り込める", async () => {
const request = new Request("http://localhost/api/users?name=山田");
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data).toHaveLength(1);
expect(data[0].name).toBe("山田太郎");
});
it("該当なしは空配列を返す", async () => {
const request = new Request("http://localhost/api/users?name=存在しない");
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data).toHaveLength(0);
});
});
POST ハンドラのテスト
describe("POST /api/users", () => {
it("正常に作成できる", async () => {
const request = new Request("http://localhost/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "鈴木一郎" }),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(201);
expect(data).toMatchObject({ name: "鈴木一郎" });
expect(typeof data.id).toBe("number");
});
it("name がない場合は 400 を返す", async () => {
const request = new Request("http://localhost/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "test@example.com" }),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe("name is required");
});
it("不正な JSON は 400 を返す", async () => {
const request = new Request("http://localhost/api/users", {
method: "POST",
body: "invalid json",
});
const response = await POST(request);
expect(response.status).toBe(400);
});
});
ポイント
- Route Handler はただの非同期関数(
(request: Request) => Promise<Response>)なので、new Request()を直接渡してテストできる。サーバーを起動する必要がない new Request(url, { method, headers, body })で任意のリクエストを組み立てられる。クエリパラメータは URL 文字列に含めて渡すresponse.json()でレスポンスボディをデシリアライズし、response.statusでステータスコードを検証するjest-mock-moduleとの使い分け: Route Handler が外部モジュール(DB・メール送信等)に依存している場合はjest.mock()と組み合わせる。このパターンはインメモリロジックのハンドラに適している- Prisma など外部依存がある場合は
jest.mock("@prisma/client")でモックしてから同じパターンで使える