記事

APIのUnitテストの入門

Node.js(Express)でTSを使っているプロダクトのtestの導入方法

やらないこと

  • CI/CDに組み込むとかはやりません

やること

  • Jest + TypeScript
  • DBモック使用
  • ローカル実行のみ
  • Unit Testのみ

最初の導入としてはこれでいい気がする
$ npm install --save-dev jest @types/jest ts-jest jest-mock-extended

jest-mock-extendedが意外とMockを生成するにはとても便利な代物な気がする

  • interfaceからMockを作ってくれる
  • Prismaをmockすると、そこに生えているメソッドがinterfaceになるので、Mockがとても楽になる
// src/service/UserService.ts
export type CreateUserInput = { name: string };
export type User = { id: string; name: string };

export interface UserRepo {
  findById(id: string): Promise<User | null>;
  create(data: CreateUserInput): Promise<User>;
}

// こういうinterfaceをMockしたらいいのさ

export class UserService {
  constructor(private readonly repo: UserRepo) {}

  async getById(id: string) {
    const u = await this.repo.findById(id);
    if (!u) throw new Error("UserNotFound");
    return u;
  }

  async register(input: CreateUserInput) {
    if (!input.name.trim()) throw new Error("InvalidName");
    return this.repo.create(input);
  }
}

Prismaを使っている人なら、PrismaをMockしたらそこから生えているメソッドを丸々Mockにできる

 it('ユーザーを正常に作成する', async () => {
      const hashedPassword = 'hashedpassword';
      const mockUser = {
        id: BigInt(1),
        email: signupData.email,
        passwordHash: hashedPassword,
        firstName: signupData.firstName,
        lastName: signupData.lastName,
        isAdmin: false,
      };

      //こんな感じで
      prismaMock.user.findUnique.mockResolvedValue(null);
      (mockedBcrypt.hash as jest.Mock).mockResolvedValue(hashedPassword);
      prismaMock.user.create.mockResolvedValue(mockUser as any);

      const result = await AuthService.signup(signupData);

      expect(prismaMock.user.findUnique).toHaveBeenCalledWith({
        where: { email: signupData.email }
      });
      expect(prismaMock.user.create).toHaveBeenCalledWith({
        data: {
          email: signupData.email,
          passwordHash: hashedPassword,
          firstName: signupData.firstName,
          lastName: signupData.lastName,
          firstNameKana: undefined,
          lastNameKana: undefined,
        }
      });
      expect(result.user.email).toBe(signupData.email);
      expect(result.token).toBeDefined();
    });

© 2026 Shotaro Kinjo