1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Vitest】TypeScript / Expressにおけるドメイン駆動設計の各層別テスト実装 Part2: アプリケーション層

Posted at

はじめに

この記事は、以下の記事の続きです。

今回は、アプリケーション層のテストを実装します。

開発環境

開発環境は以下の通りです。

  • Windows11
  • Docker Engine 27.0.3
  • Docker Compose 2
  • PostgreSQL 18.1
  • Node.js 24.11.0
  • npm 11.6.2
  • TypeScript 5.9.3
  • Express 5.1.0
  • Prisma 6.18.0
  • Zod 4.1.12
  • Vitest 4.0.14

アプリケーション層のテスト

アプリケーション層のテストでは、リポジトリをモックにして、ユースケースのビジネスロジックをテストします。

アプリケーション層テストのポイント

アプリケーション層のテストでは、リポジトリをモックに差し替えて、ユースケースのビジネスロジックに焦点を当てます。

項目 説明
テスト対象 ユースケースのビジネスフロー
モック対象 リポジトリ(データベース操作)
検証内容 ビジネスルールが正しく実行されるか
データベース 不要(モックを使用)

モックとは

モック(Mock) とは、テスト時に本物の実装を模倣する偽のオブジェクトです。

モックを使う理由

// モックなし: 実際のデータベースが必要
const repository = new PrismaUserRepository(prisma); // PostgreSQLが必要
const useCase = new CreateUserUseCase(repository);

// モックあり: データベース不要
const mockRepository: IUserRepository = {
  save: vi.fn(), // 偽の実装
  findById: vi.fn(),
  // ...
};
const useCase = new CreateUserUseCase(mockRepository); // データベース不要

以下のようなメリットがあります。

  • データベース不要でテストが高速
  • テストの独立性が保たれる
  • 特定のケース(エラーなど)を再現しやすい

Vitestのモック機能

Vitestではvi.fn()を使ってモック関数を作成します。

// モック関数の作成
const mockFunction = vi.fn();

// 戻り値を指定
const mockFunction = vi.fn().mockReturnValue('result');

// Promiseを返す
const mockFunction = vi.fn().mockResolvedValue('result');

// エラーをスロー
const mockFunction = vi.fn().mockRejectedValue(new Error('error'));

// 呼び出しを検証
expect(mockFunction).toHaveBeenCalled();
expect(mockFunction).toHaveBeenCalledWith('arg');

ユーザー作成ユースケースのテスト

__tests__/application/usecases/CreateUserUseCase.test.ts
import { beforeEach, describe, expect, it, vi } from "vitest";
import { IUserRepository } from "../../../src/domain/repositories/IUserRepository";
import { CreateUserUseCase } from "../../../src/application/usecases/CreateUserUseCase";
import { User } from "../../../src/domain/entities/User";
import { UserId } from "../../../src/domain/value-objects/UserId";
import { Email } from "../../../src/domain/value-objects/Email";
import { UserName } from "../../../src/domain/value-objects/UserName";

describe("CreateUserUseCase", () => {
  let mockRepository: IUserRepository;
  let useCase: CreateUserUseCase;

  beforeEach(() => {
    // 各テストの前にモックを初期化(前のテストの影響を受けないようにする)
    // vi.fn()で全てのメソッドをモック関数に
    mockRepository = {
      save: vi.fn(),
      findById: vi.fn(),
      findAll: vi.fn(),
      update: vi.fn(),
      delete: vi.fn(),
      existsByEmail: vi.fn(),
    };

    useCase = new CreateUserUseCase(mockRepository);
  });

  it("ユーザーを作成できる", async () => {
    // AAA(Arrange-Act-Assert)パターンでテスト
    // Arrange: テストデータの準備
    const input = {
      email: "test@example.com",
      name: "Test User",
    };

    /** 保存後に返されるユーザー(IDが割り当てられている) */
    const savedUser = User.reconstruct(
      new UserId(1),
      new Email("test@example.com"),
      new UserName("Test User")
    );

    // モックの振る舞いを定義
    vi.mocked(mockRepository.existsByEmail).mockResolvedValue(false);
    vi.mocked(mockRepository.save).mockResolvedValue(savedUser);

    // Act: ユースケースの実行
    const result = await useCase.execute(input);

    // Assert: 結果の検証
    expect(result).toEqual({
      id: 1,
      email: "test@example.com",
      name: "Test User",
    });

    // モックが正しく呼ばれたか検証
    expect(mockRepository.existsByEmail).toHaveBeenCalledTimes(1);
    expect(mockRepository.save).toHaveBeenCalledTimes(1);
  });

  it("メールアドレスが既に存在する場合はエラーをスローする", async () => {
    // Arrange
    const input = {
      email: "existing@example.com",
      name: "Test User",
    };

    // メールアドレスが既に存在する場合
    vi.mocked(mockRepository.existsByEmail).mockResolvedValue(true);

    // Act & Assert
    await expect(useCase.execute(input)).rejects.toThrow(
      "Email already exists"
    );

    // saveは呼ばれないことを検証
    expect(mockRepository.save).not.toHaveBeenCalled();
  });
});

全ユーザー取得ユースケースのテスト

__tests__/application/usecases/GetAllUsersUseCase.test.ts
import { beforeEach, describe, expect, it, vi } from "vitest";
import { IUserRepository } from "../../../src/domain/repositories/IUserRepository";
import { GetAllUsersUseCase } from "../../../src/application/usecases/GetAllUsersUseCase";
import { User } from "../../../src/domain/entities/User";
import { UserId } from "../../../src/domain/value-objects/UserId";
import { Email } from "../../../src/domain/value-objects/Email";
import { UserName } from "../../../src/domain/value-objects/UserName";

describe("GetAllUsersUseCase", () => {
  let mockRepository: IUserRepository;
  let useCase: GetAllUsersUseCase;

  beforeEach(() => {
    mockRepository = {
      save: vi.fn(),
      findById: vi.fn(),
      findAll: vi.fn(),
      update: vi.fn(),
      delete: vi.fn(),
      existsByEmail: vi.fn(),
    };

    useCase = new GetAllUsersUseCase(mockRepository);
  });

  it("ユーザーを取得できる", async () => {
    // Arrange: テストデータの準備
    const users = [
      {
        id: 1,
        email: "test@example.com",
        name: "Test User",
      },
      {
        id: 2,
        email: "test2@example.com",
        name: "Test User2",
      },
    ].map((user) =>
      User.reconstruct(
        new UserId(user.id),
        new Email(user.email),
        new UserName(user.name)
      )
    );

    vi.mocked(mockRepository.findAll).mockResolvedValue(users);

    // Act
    const result = await useCase.execute();

    // Assert
    expect(result).toEqual({
      users: [
        {
          id: 1,
          email: "test@example.com",
          name: "Test User",
        },
        {
          id: 2,
          email: "test2@example.com",
          name: "Test User2",
        },
      ],
    });
    expect(mockRepository.findAll).toHaveBeenCalledTimes(1);
  });
});

ユーザー取得ユースケースのテスト

__tests__/application/usecases/GetUserUseCase.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { GetUserUseCase } from "../../../src/application/usecases/GetUserUseCase";
import { IUserRepository } from "../../../src/domain/repositories/IUserRepository";
import { User } from "../../../src/domain/entities/User";
import { Email } from "../../../src/domain/value-objects/Email";
import { UserName } from "../../../src/domain/value-objects/UserName";
import { UserId } from "../../../src/domain/value-objects/UserId";

describe("GetUserUseCase", () => {
  let mockRepository: IUserRepository;
  let useCase: GetUserUseCase;

  beforeEach(() => {
    mockRepository = {
      save: vi.fn(),
      findById: vi.fn(),
      findAll: vi.fn(),
      update: vi.fn(),
      delete: vi.fn(),
      existsByEmail: vi.fn(),
    };

    useCase = new GetUserUseCase(mockRepository);
  });

  it("ユーザーを取得できる", async () => {
    // Arrange
    const input = { id: 1 };

    const user = User.reconstruct(
      new UserId(1),
      new Email("test@example.com"),
      new UserName("Test User")
    );

    vi.mocked(mockRepository.findById).mockResolvedValue(user);

    // Act
    const result = await useCase.execute(input);

    // Assert
    expect(result).toEqual({
      id: 1,
      email: "test@example.com",
      name: "Test User",
    });
    expect(mockRepository.findById).toHaveBeenCalledWith(
      expect.objectContaining({ value: 1 })
    );
  });

  it("ユーザーが存在しない場合はnullを返す", async () => {
    // Arrange
    const input = { id: 999 };

    vi.mocked(mockRepository.findById).mockResolvedValue(null);

    // Act
    const result = await useCase.execute(input);

    // Assert
    expect(result).toBeNull();
  });
});

ユーザー更新ユースケースのテスト

__tests__/application/usecases/UpdateUserUseCase.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { UpdateUserUseCase } from "../../../src/application/usecases/UpdateUserUseCase";
import { IUserRepository } from "../../../src/domain/repositories/IUserRepository";
import { User } from "../../../src/domain/entities/User";
import { Email } from "../../../src/domain/value-objects/Email";
import { UserName } from "../../../src/domain/value-objects/UserName";
import { UserId } from "../../../src/domain/value-objects/UserId";

describe("UpdateUserUseCase", () => {
  let mockRepository: IUserRepository;
  let useCase: UpdateUserUseCase;

  beforeEach(() => {
    mockRepository = {
      save: vi.fn(),
      findById: vi.fn(),
      findAll: vi.fn(),
      update: vi.fn(),
      delete: vi.fn(),
      existsByEmail: vi.fn(),
    };

    useCase = new UpdateUserUseCase(mockRepository);
  });

  it("ユーザー情報を更新できる", async () => {
    // Arrange
    const existingUser = User.reconstruct(
      new UserId(1),
      new Email("old@example.com"),
      new UserName("Old Name")
    );

    const input = {
      id: 1,
      email: "new@example.com",
      name: "New Name",
    };

    vi.mocked(mockRepository.findById).mockResolvedValue(existingUser);
    vi.mocked(mockRepository.existsByEmail).mockResolvedValue(false);
    vi.mocked(mockRepository.update).mockResolvedValue(existingUser);

    // Act
    const result = await useCase.execute(input);

    // Assert
    expect(result.email).toBe("new@example.com");
    expect(result.name).toBe("New Name");
    expect(mockRepository.update).toHaveBeenCalledTimes(1);
  });

  it("存在しないユーザーを更新しようとするとエラーをスローする", async () => {
    // Arrange
    const input = {
      id: 999,
      email: "test@example.com",
      name: "Test User",
    };

    vi.mocked(mockRepository.findById).mockResolvedValue(null);

    // Act & Assert
    await expect(useCase.execute(input)).rejects.toThrow("User not found");
    expect(mockRepository.update).not.toHaveBeenCalled();
  });

  it("メールアドレスが既に使用されている場合はエラーをスローする", async () => {
    // Arrange
    const existingUser = User.reconstruct(
      new UserId(1),
      new Email("old@example.com"),
      new UserName("Test User")
    );

    const input = {
      id: 1,
      email: "existing@example.com",
      name: "Test User",
    };

    vi.mocked(mockRepository.findById).mockResolvedValue(existingUser);
    vi.mocked(mockRepository.existsByEmail).mockResolvedValue(true);

    // Act & Assert
    await expect(useCase.execute(input)).rejects.toThrow(
      "Email already exists"
    );
    expect(mockRepository.update).not.toHaveBeenCalled();
  });
});

ユーザー削除ユースケースのテスト

__tests__/application/usecases/DeleteUserUseCase.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { DeleteUserUseCase } from "../../../src/application/usecases/DeleteUserUseCase";
import { IUserRepository } from "../../../src/domain/repositories/IUserRepository";
import { User } from "../../../src/domain/entities/User";
import { Email } from "../../../src/domain/value-objects/Email";
import { UserName } from "../../../src/domain/value-objects/UserName";
import { UserId } from "../../../src/domain/value-objects/UserId";

describe("DeleteUserUseCase", () => {
  let mockRepository: IUserRepository;
  let useCase: DeleteUserUseCase;

  beforeEach(() => {
    mockRepository = {
      save: vi.fn(),
      findById: vi.fn(),
      findAll: vi.fn(),
      update: vi.fn(),
      delete: vi.fn(),
      existsByEmail: vi.fn(),
    };

    useCase = new DeleteUserUseCase(mockRepository);
  });

  it("ユーザーを削除できる", async () => {
    // Arrange
    const input = { id: 1 };

    const user = User.reconstruct(
      new UserId(1),
      new Email("test@example.com"),
      new UserName("Test User")
    );

    vi.mocked(mockRepository.findById).mockResolvedValue(user);
    vi.mocked(mockRepository.delete).mockResolvedValue(undefined);

    // Act
    await useCase.execute(input);

    // Assert
    expect(mockRepository.findById).toHaveBeenCalledWith(
      expect.objectContaining({ value: 1 })
    );
    expect(mockRepository.delete).toHaveBeenCalledWith(
      expect.objectContaining({ value: 1 })
    );
  });

  it("存在しないユーザーを削除しようとするとエラーをスローする", async () => {
    // Arrange
    const input = { id: 999 };

    vi.mocked(mockRepository.findById).mockResolvedValue(null);

    // Act & Assert
    await expect(useCase.execute(input)).rejects.toThrow("User not found");
    expect(mockRepository.delete).not.toHaveBeenCalled();
  });
});

まとめ

今回は、アプリケーション層(ユースケース)のテストを実装しました。

  • ポイント
    • モックを使用: リポジトリをモックに差し替えてデータベース不要に
    • ビジネスロジックに集中: データベース操作ではなく、ビジネスフローをテスト
    • AAAパターン: Arrange-Act-Assertで構造化されたテスト
    • モックの検証: メソッドが正しく呼ばれたかを確認
  • モックのメリット
    • データベース不要で高速なテスト実行
    • テストの独立性が保たれる
    • エラーケースの再現が容易
    • ビジネスロジックに集中できる

次回は、インフラ層のテストを実装します。インメモリリポジトリを使って、リポジトリの実装をテストします。

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?