awesome-hacks
Docs

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 -- --coverage
PASS  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までの全経路を通したテストを実装する。

参考