NestJSのユニットテスト
TestingModule を使って Service・Controller・Guard を単体で検証する実践パターンを学ぶ
最終更新:2026/06/08
NestJSテスト入門でJestの基本設定と最小テストを確認している前提。
この記事では、UsersService の網羅的な単体テスト、UsersController のテスト、JWT認証Guardのテストを実装する。
この記事でやること
UsersServiceの全メソッド(findAll / findOne / create / update / remove)を単体テストするUsersControllerが Service に正しく委譲しているかテストするJwtAuthGuardを単体でテストする- カバレッジレポートを確認する
触るファイル一覧
api-sample/
└─ src/
├─ users/
│ ├─ users.service.spec.ts # Service の全メソッドをテスト
│ └─ users.controller.spec.ts # Controller が Service を呼ぶかテスト
└─ auth/
└─ jwt-auth.guard.spec.ts # Guard の canActivate をテスト1. UsersService の全メソッドをテストする
// =============================================================================
// ファイル: src/users/users.service.spec.ts
// 実行: pnpm test users.service
// =============================================================================
import { BadRequestException, NotFoundException } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
import { PrismaService } from "../prisma/prisma.service";
import { UsersService } from "./users.service";
describe("UsersService", () => {
let service: UsersService;
const prisma = {
user: {
findMany: jest.fn(),
findUnique: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
};
beforeEach(async () => {
jest.clearAllMocks();
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{ provide: PrismaService, useValue: prisma },
],
}).compile();
service = module.get(UsersService);
});
// ─────────────────────────────────────────────────────────────────────────
// findAll
// ─────────────────────────────────────────────────────────────────────────
describe("findAll", () => {
it("ユーザー一覧を返す", async () => {
const users = [
{ id: "u_1", email: "taro@example.com", name: "Taro", createdAt: new Date(), updatedAt: new Date() },
];
prisma.user.findMany.mockResolvedValueOnce(users);
await expect(service.findAll()).resolves.toEqual(users);
expect(prisma.user.findMany).toHaveBeenCalledWith({
orderBy: { createdAt: "desc" },
select: { id: true, email: true, name: true, createdAt: true, updatedAt: true },
});
});
it("ユーザーが0件でも空配列を返す", async () => {
prisma.user.findMany.mockResolvedValueOnce([]);
await expect(service.findAll()).resolves.toEqual([]);
});
});
// ─────────────────────────────────────────────────────────────────────────
// findOne
// ─────────────────────────────────────────────────────────────────────────
describe("findOne", () => {
it("存在するユーザーを返す", async () => {
const user = { id: "u_1", email: "taro@example.com", name: "Taro", createdAt: new Date(), updatedAt: new Date() };
prisma.user.findUnique.mockResolvedValueOnce(user);
await expect(service.findOne("u_1")).resolves.toEqual(user);
expect(prisma.user.findUnique).toHaveBeenCalledWith({
where: { id: "u_1" },
select: { id: true, email: true, name: true, createdAt: true, updatedAt: true },
});
});
it("存在しない場合 NotFoundException を投げる", async () => {
prisma.user.findUnique.mockResolvedValueOnce(null);
await expect(service.findOne("not-exist")).rejects.toBeInstanceOf(NotFoundException);
});
});
// ─────────────────────────────────────────────────────────────────────────
// create
// ─────────────────────────────────────────────────────────────────────────
describe("create", () => {
it("ユーザーを作成して返す", async () => {
const created = { id: "u_2", email: "hanako@example.com", name: "Hanako", createdAt: new Date(), updatedAt: new Date() };
prisma.user.create.mockResolvedValueOnce(created);
const dto = { email: "hanako@example.com", name: "Hanako", password: "password123" };
const result = await service.create(dto);
expect(result).toMatchObject({ email: "hanako@example.com", name: "Hanako" });
// パスワードはレスポンスに含まれない
expect((result as Record<string, unknown>).passwordHash).toBeUndefined();
});
it("メールアドレス重複時は BadRequestException を投げる", async () => {
// Prismaがユニーク制約エラーを投げた場合のシミュレート
prisma.user.create.mockRejectedValueOnce({ code: "P2002" });
const dto = { email: "dup@example.com", name: "Dup", password: "password123" };
await expect(service.create(dto)).rejects.toBeInstanceOf(BadRequestException);
});
});
// ─────────────────────────────────────────────────────────────────────────
// update
// ─────────────────────────────────────────────────────────────────────────
describe("update", () => {
it("ユーザーを更新して返す", async () => {
const updated = { id: "u_1", email: "taro@example.com", name: "Taro Updated", createdAt: new Date(), updatedAt: new Date() };
prisma.user.findUnique.mockResolvedValueOnce({ id: "u_1" }); // 存在確認
prisma.user.update.mockResolvedValueOnce(updated);
await expect(service.update("u_1", { name: "Taro Updated" })).resolves.toMatchObject({ name: "Taro Updated" });
});
it("存在しない場合 NotFoundException を投げる", async () => {
prisma.user.findUnique.mockResolvedValueOnce(null);
await expect(service.update("no-exist", { name: "X" })).rejects.toBeInstanceOf(NotFoundException);
});
});
// ─────────────────────────────────────────────────────────────────────────
// remove
// ─────────────────────────────────────────────────────────────────────────
describe("remove", () => {
it("ユーザーを削除する", async () => {
prisma.user.findUnique.mockResolvedValueOnce({ id: "u_1" }); // 存在確認
prisma.user.delete.mockResolvedValueOnce({ id: "u_1" });
await expect(service.remove("u_1")).resolves.not.toThrow();
expect(prisma.user.delete).toHaveBeenCalledWith({ where: { id: "u_1" } });
});
it("存在しない場合 NotFoundException を投げる", async () => {
prisma.user.findUnique.mockResolvedValueOnce(null);
await expect(service.remove("no-exist")).rejects.toBeInstanceOf(NotFoundException);
});
});
});BadRequestException を投げるためのService側実装
create でPrismaのユニーク制約エラー(P2002)をキャッチして BadRequestException に変換する例。
// src/users/users.service.ts(createメソッドのエラーハンドリング部分)
async create(dto: CreateUserDto) {
try {
const user = await this.prisma.user.create({ ... });
return user;
} catch (e: unknown) {
if (typeof e === "object" && e !== null && (e as { code?: string }).code === "P2002") {
throw new BadRequestException("このメールアドレスはすでに使われています");
}
throw e;
}
}ユニットテストは「Serviceがこのエラーを受け取ったとき、BadRequestException に変換するか」を確認している。Prisma実装の詳細には立ち入らない。
2. UsersController をテストする
ControllerのテストはServiceへの委譲が正しいかを確認する。Serviceの中身は jest.fn() で置き換え、ロジックはテストしない。
// =============================================================================
// ファイル: src/users/users.controller.spec.ts
// 実行: pnpm test users.controller
// =============================================================================
import { NotFoundException } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
import { UsersController } from "./users.controller";
import { UsersService } from "./users.service";
describe("UsersController", () => {
let controller: UsersController;
const mockService = {
findAll: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
};
beforeEach(async () => {
jest.clearAllMocks();
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [{ provide: UsersService, useValue: mockService }],
}).compile();
controller = module.get(UsersController);
});
it("コントローラーが定義されている", () => {
expect(controller).toBeDefined();
});
describe("findAll", () => {
it("UsersService.findAll の結果をそのまま返す", async () => {
const users = [{ id: "u_1", email: "taro@example.com" }];
mockService.findAll.mockResolvedValueOnce(users);
const result = await controller.findAll();
expect(result).toEqual(users);
expect(mockService.findAll).toHaveBeenCalledTimes(1);
});
});
describe("findOne", () => {
it("UsersService.findOne に id を渡す", async () => {
const user = { id: "u_1", email: "taro@example.com" };
mockService.findOne.mockResolvedValueOnce(user);
const result = await controller.findOne("u_1");
expect(result).toEqual(user);
expect(mockService.findOne).toHaveBeenCalledWith("u_1");
});
it("Service が NotFoundException を投げたらそのまま伝播する", async () => {
mockService.findOne.mockRejectedValueOnce(new NotFoundException());
await expect(controller.findOne("no-exist")).rejects.toBeInstanceOf(NotFoundException);
});
});
describe("create", () => {
it("UsersService.create に DTO を渡す", async () => {
const dto = { email: "hanako@example.com", name: "Hanako", password: "pw123" };
const created = { id: "u_2", ...dto };
mockService.create.mockResolvedValueOnce(created);
const result = await controller.create(dto as never);
expect(result).toEqual(created);
expect(mockService.create).toHaveBeenCalledWith(dto);
});
});
describe("remove", () => {
it("UsersService.remove に id を渡す", async () => {
mockService.remove.mockResolvedValueOnce(undefined);
await controller.remove("u_1");
expect(mockService.remove).toHaveBeenCalledWith("u_1");
});
});
});3. JwtAuthGuard をテストする
Guardのテストは canActivate の戻り値を確認する。実際のJWT検証は JwtStrategy に委譲されているため、Guard単体では「Guardが存在するか」と「Guardのファクトリが正しいか」を確認するパターンが多い。
// =============================================================================
// ファイル: src/auth/jwt-auth.guard.spec.ts
// 実行: pnpm test jwt-auth.guard
// =============================================================================
import { ExecutionContext } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Test, TestingModule } from "@nestjs/testing";
import { AuthGuard } from "@nestjs/passport";
import { JwtAuthGuard } from "./jwt-auth.guard";
describe("JwtAuthGuard", () => {
let guard: JwtAuthGuard;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [JwtAuthGuard, Reflector],
}).compile();
guard = module.get(JwtAuthGuard);
});
it("guard が定義されている", () => {
expect(guard).toBeDefined();
});
it("AuthGuard('jwt') を継承している", () => {
expect(guard).toBeInstanceOf(AuthGuard("jwt"));
});
});実際の認証フロー(トークン検証・ユーザー注入)の確認はE2Eテストで行う。Guard単体テストは「クラスが存在する」「継承が正しい」といった最低限の確認にとどめることが多い。
4. テストを実行する
# 全ての spec.ts を実行
pnpm test
# ファイル名で絞り込む
pnpm test users.service
pnpm test users.controller
pnpm test jwt-auth.guard
# カバレッジレポートを出力
pnpm test -- --coveragePASS src/users/users.service.spec.ts
UsersService
findAll
✓ ユーザー一覧を返す (12 ms)
✓ ユーザーが0件でも空配列を返す (2 ms)
findOne
✓ 存在するユーザーを返す (3 ms)
✓ 存在しない場合 NotFoundException を投げる (2 ms)
create
✓ ユーザーを作成して返す (3 ms)
✓ メールアドレス重複時は BadRequestException を投げる (2 ms)
update
✓ ユーザーを更新して返す (2 ms)
✓ 存在しない場合 NotFoundException を投げる (1 ms)
remove
✓ ユーザーを削除する (2 ms)
✓ 存在しない場合 NotFoundException を投げる (1 ms)
PASS src/users/users.controller.spec.ts
...実装タスク(チェックリスト)
-
src/users/users.service.spec.tsを全メソッド対応に更新 -
src/users/users.controller.spec.tsを作成 -
src/auth/jwt-auth.guard.spec.tsを作成(Guardがある場合) -
pnpm testが全てPASSする状態にする -
pnpm test -- --coverageでカバレッジを確認する
次のステップ
NestJS E2Eテストで、HTTP入口からDBまでの全経路を通したテストを実装する。