1
1

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におけるドメイン駆動設計の各層別テスト実装 Part4: プレゼンテーション層

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

プレゼンテーション層のテスト

プレゼンテーション層(コントローラー)のテストでは、HTTPリクエスト/レスポンスの処理が正しく動作するかを検証します。
ユースケースをモックに差し替えて、HTTPの詳細に焦点を当てます。

項目 説明
テスト対象 HTTPリクエスト/レスポンスの処理
モック対象 ユースケース(ビジネスロジック)
検証内容 ステータスコード、レスポンスボディ
データベース 不要(ユースケースをモック)

ユーザー作成のテスト

ユーザー作成エンドポイントのテストを実装します。

__tests__/presentation/controllers/UserController.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { UserController } from "../../../src/presentation/controllers/UserController";
import { CreateUserUseCase } from "../../../src/application/usecases/CreateUserUseCase";
import { GetUserUseCase } from "../../../src/application/usecases/GetUserUseCase";
import { GetAllUsersUseCase } from "../../../src/application/usecases/GetAllUsersUseCase";
import { UpdateUserUseCase } from "../../../src/application/usecases/UpdateUserUseCase";
import { DeleteUserUseCase } from "../../../src/application/usecases/DeleteUserUseCase";
import { Request, Response } from "express";

describe("UserController", () => {
  let mockCreateUserUseCase: CreateUserUseCase;
  let mockGetUserUseCase: GetUserUseCase;
  let mockGetAllUsersUseCase: GetAllUsersUseCase;
  let mockUpdateUserUseCase: UpdateUserUseCase;
  let mockDeleteUserUseCase: DeleteUserUseCase;

  let controller: UserController;
  let mockRequest: Partial<Request>;
  let mockResponse: Partial<Response>;

  beforeEach(() => {
    // モックユースケースの作成
    mockCreateUserUseCase = {
      execute: vi.fn(),
    } as unknown as CreateUserUseCase;

    mockGetUserUseCase = {
      execute: vi.fn(),
    } as unknown as GetUserUseCase;

    mockGetAllUsersUseCase = {
      execute: vi.fn(),
    } as unknown as GetAllUsersUseCase;

    mockUpdateUserUseCase = {
      execute: vi.fn(),
    } as unknown as UpdateUserUseCase;

    mockDeleteUserUseCase = {
      execute: vi.fn(),
    } as unknown as DeleteUserUseCase;

    // コントローラーの作成
    controller = new UserController(
      mockCreateUserUseCase,
      mockGetUserUseCase,
      mockGetAllUsersUseCase,
      mockUpdateUserUseCase,
      mockDeleteUserUseCase
    );

    // モックリクエスト・レスポンスの作成
    mockRequest = {
      body: {},
      params: {},
    };

    mockResponse = {
      status: vi.fn().mockReturnThis(),
      json: vi.fn().mockReturnThis(),
      send: vi.fn().mockReturnThis(),
    };
  });

  describe("create", () => {
    it("ユーザーを作成して201を返す", async () => {
      // Arrange
      mockRequest.body = {
        email: "test@example.com",
        name: "Test User",
      };

      vi.mocked(mockCreateUserUseCase.execute).mockResolvedValue({
        id: 1,
        email: "test@example.com",
        name: "Test User",
      });

      // Act
      await controller.create(mockRequest as Request, mockResponse as Response);

      // Assert
      expect(mockResponse.status).toHaveBeenCalledWith(201);
      expect(mockResponse.json).toHaveBeenCalledWith({
        id: 1,
        email: "test@example.com",
        name: "Test User",
      });
    });

    it("ユースケースがエラーをスローした場合は400を返す", async () => {
      // Arrange
      mockRequest.body = {
        email: "test@example.com",
        name: "Test User",
      };

      vi.mocked(mockCreateUserUseCase.execute).mockRejectedValue(
        new Error("Email already exists")
      );

      // Act
      await controller.create(mockRequest as Request, mockResponse as Response);

      // Assert
      expect(mockResponse.status).toHaveBeenCalledWith(400);
      expect(mockResponse.json).toHaveBeenCalledWith({
        error: "Email already exists",
      });
    });

    it("予期しないエラーの場合は500を返す", async () => {
      // Arrange
      mockRequest.body = {
        email: "test@example.com",
        name: "Test User",
      };

      vi.mocked(mockCreateUserUseCase.execute).mockRejectedValue(
        "Unknown error"
      );

      // Act
      await controller.create(mockRequest as Request, mockResponse as Response);

      // Assert
      expect(mockResponse.status).toHaveBeenCalledWith(500);
      expect(mockResponse.json).toHaveBeenCalledWith({
        error: "Failed to create user",
      });
    });
  });
});

テストコードの実装

__tests__/presentation/controllers/UserController.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { UserController } from "../../../src/presentation/controllers/UserController";
import { CreateUserUseCase } from "../../../src/application/usecases/CreateUserUseCase";
import { GetUserUseCase } from "../../../src/application/usecases/GetUserUseCase";
import { GetAllUsersUseCase } from "../../../src/application/usecases/GetAllUsersUseCase";
import { UpdateUserUseCase } from "../../../src/application/usecases/UpdateUserUseCase";
import { DeleteUserUseCase } from "../../../src/application/usecases/DeleteUserUseCase";
import { Request, Response } from "express";
import {
  CreateUserInput,
  UpdateUserInput,
  UserIdParam,
} from "../../../src/schemas/user.schema";

describe("UserController", () => {
  let mockCreateUserUseCase: CreateUserUseCase;
  let mockGetUserUseCase: GetUserUseCase;
  let mockGetAllUsersUseCase: GetAllUsersUseCase;
  let mockUpdateUserUseCase: UpdateUserUseCase;
  let mockDeleteUserUseCase: DeleteUserUseCase;

  let controller: UserController;
  let mockRequest: Partial<Request>;
  let mockResponse: Partial<Response>;

  beforeEach(() => {
    // モックユースケースの作成
    mockCreateUserUseCase = {
      execute: vi.fn(),
    } as unknown as CreateUserUseCase;

    mockGetUserUseCase = {
      execute: vi.fn(),
    } as unknown as GetUserUseCase;

    mockGetAllUsersUseCase = {
      execute: vi.fn(),
    } as unknown as GetAllUsersUseCase;

    mockUpdateUserUseCase = {
      execute: vi.fn(),
    } as unknown as UpdateUserUseCase;

    mockDeleteUserUseCase = {
      execute: vi.fn(),
    } as unknown as DeleteUserUseCase;

    // コントローラーの作成
    controller = new UserController(
      mockCreateUserUseCase,
      mockGetUserUseCase,
      mockGetAllUsersUseCase,
      mockUpdateUserUseCase,
      mockDeleteUserUseCase
    );
  });

  describe("create", () => {
    it("ユーザーを作成して201を返す", async () => {
      // Arrange
      const mockRequest = {
        body: {
          email: "test@example.com",
          name: "Test User",
        },
      } as Request<{}, {}, CreateUserInput>;

      const mockResponse = {
        status: vi.fn().mockReturnThis(),
        json: vi.fn().mockReturnThis(),
      } as unknown as Response;

      vi.mocked(mockCreateUserUseCase.execute).mockResolvedValue({
        id: 1,
        email: "test@example.com",
        name: "Test User",
      });

      // Act
      await controller.create(mockRequest, mockResponse);

      // Assert
      expect(mockResponse.status).toHaveBeenCalledWith(201);
      expect(mockResponse.json).toHaveBeenCalledWith({
        id: 1,
        email: "test@example.com",
        name: "Test User",
      });
    });

    it("ユースケースがエラーをスローした場合は400を返す", async () => {
      // Arrange
      const mockRequest = {
        body: {
          email: "test@example.com",
          name: "Test User",
        },
      } as Request<{}, {}, CreateUserInput>;

      const mockResponse = {
        status: vi.fn().mockReturnThis(),
        json: vi.fn().mockReturnThis(),
      } as unknown as Response;

      vi.mocked(mockCreateUserUseCase.execute).mockRejectedValue(
        new Error("Email already exists")
      );

      // Act
      await controller.create(mockRequest, mockResponse);

      // Assert
      expect(mockResponse.status).toHaveBeenCalledWith(400);
      expect(mockResponse.json).toHaveBeenCalledWith({
        error: "Email already exists",
      });
    });

    it("予期しないエラーの場合は500を返す", async () => {
      // Arrange
      const mockRequest = {
        body: {
          email: "test@example.com",
          name: "Test User",
        },
      } as Request<{}, {}, CreateUserInput>;

      const mockResponse = {
        status: vi.fn().mockReturnThis(),
        json: vi.fn().mockReturnThis(),
      } as unknown as Response;

      vi.mocked(mockCreateUserUseCase.execute).mockRejectedValue(
        "Unknown error"
      );

      // Act
      await controller.create(mockRequest, mockResponse);

      // Assert
      expect(mockResponse.status).toHaveBeenCalledWith(500);
      expect(mockResponse.json).toHaveBeenCalledWith({
        error: "Failed to create user",
      });
    });
  });

  describe("getAll", () => {
    it("全てのユーザーを取得して200を返す", async () => {
      // Arrange
      const mockRequest = {} as Request;

      const mockResponse = {
        json: vi.fn(),
      } as unknown as Response;

      vi.mocked(mockGetAllUsersUseCase.execute).mockResolvedValue({
        users: [
          { id: 1, email: "user1@example.com", name: "User 1" },
          { id: 2, email: "user2@example.com", name: "User 2" },
        ],
      });

      // Act
      await controller.getAll(mockRequest, mockResponse);

      // Assert
      expect(mockResponse.json).toHaveBeenCalledWith({
        users: [
          { id: 1, email: "user1@example.com", name: "User 1" },
          { id: 2, email: "user2@example.com", name: "User 2" },
        ],
      });
    });

    it("ユーザーが存在しない場合は空配列を返す", async () => {
      // Arrange
      const mockRequest = {} as Request;

      const mockResponse = {
        json: vi.fn(),
      } as unknown as Response;

      vi.mocked(mockGetAllUsersUseCase.execute).mockResolvedValue({
        users: [],
      });

      // Act
      await controller.getAll(mockRequest, mockResponse);

      // Assert
      expect(mockResponse.json).toHaveBeenCalledWith({
        users: [],
      });
    });
  });

  describe("getById", () => {
    it("ユーザーが存在する場合は200を返す", async () => {
      // Arrange
      const mockRequest = {
        params: { id: "1" },
      } as Request<UserIdParam>;

      const mockResponse = {
        json: vi.fn(),
      } as unknown as Response;

      vi.mocked(mockGetUserUseCase.execute).mockResolvedValue({
        id: 1,
        email: "test@example.com",
        name: "Test User",
      });

      // Act
      await controller.getById(mockRequest, mockResponse);

      // Assert
      expect(mockResponse.json).toHaveBeenCalledWith({
        id: 1,
        email: "test@example.com",
        name: "Test User",
      });
      // status(200)は明示的に呼ばれないため、検証しない
    });

    it("ユーザーが存在しない場合は404を返す", async () => {
      // Arrange
      const mockRequest = {
        params: { id: "999" },
      } as Request<UserIdParam>;

      const mockResponse = {
        status: vi.fn().mockReturnThis(),
        json: vi.fn(),
      } as unknown as Response;

      vi.mocked(mockGetUserUseCase.execute).mockResolvedValue(null);

      // Act
      await controller.getById(mockRequest, mockResponse);

      // Assert
      expect(mockResponse.status).toHaveBeenCalledWith(404);
      expect(mockResponse.json).toHaveBeenCalledWith({
        error: "User not found",
      });
    });

    it("ユースケースがエラーをスローした場合は400を返す", async () => {
      // Arrange
      const mockRequest = {
        params: { id: "invalid" },
      } as Request<UserIdParam>;

      const mockResponse = {
        status: vi.fn().mockReturnThis(),
        json: vi.fn(),
      } as unknown as Response;

      vi.mocked(mockGetUserUseCase.execute).mockRejectedValue(
        new Error("User ID must be a positive number")
      );

      // Act
      await controller.getById(mockRequest, mockResponse);

      // Assert
      expect(mockResponse.status).toHaveBeenCalledWith(400);
      expect(mockResponse.json).toHaveBeenCalledWith({
        error: "User ID must be a positive number",
      });
    });
  });

  describe("update", () => {
    it("ユーザー情報を更新して200を返す", async () => {
      // Arrange
      const mockRequest = {
        params: { id: "1" },
        body: {
          email: "new@example.com",
          name: "New Name",
        },
      } as Request<UserIdParam, {}, UpdateUserInput>;

      const mockResponse = {
        json: vi.fn(),
      } as unknown as Response;

      vi.mocked(mockUpdateUserUseCase.execute).mockResolvedValue({
        id: 1,
        email: "new@example.com",
        name: "New Name",
      });

      // Act
      await controller.update(mockRequest, mockResponse);

      // Assert
      expect(mockResponse.json).toHaveBeenCalledWith({
        id: 1,
        email: "new@example.com",
        name: "New Name",
      });
    });

    it("ユーザーが存在しない場合は400を返す", async () => {
      // Arrange
      const mockRequest = {
        params: { id: "999" },
        body: {
          email: "test@example.com",
          name: "Test User",
        },
      } as Request<UserIdParam, {}, UpdateUserInput>;

      const mockResponse = {
        status: vi.fn().mockReturnThis(),
        json: vi.fn(),
      } as unknown as Response;

      vi.mocked(mockUpdateUserUseCase.execute).mockRejectedValue(
        new Error("User not found")
      );

      // Act
      await controller.update(mockRequest, mockResponse);

      // Assert
      expect(mockResponse.status).toHaveBeenCalledWith(400);
      expect(mockResponse.json).toHaveBeenCalledWith({
        error: "User not found",
      });
    });
  });

  describe("delete", () => {
    it("ユーザーを削除して204を返す", async () => {
      // Arrange
      const mockRequest = {
        params: { id: "1" },
      } as Request<UserIdParam>;

      const mockResponse = {
        status: vi.fn().mockReturnThis(),
        send: vi.fn(),
      } as unknown as Response;

      vi.mocked(mockDeleteUserUseCase.execute).mockResolvedValue(undefined);

      // Act
      await controller.delete(mockRequest, mockResponse);

      // Assert
      expect(mockResponse.status).toHaveBeenCalledWith(204);
      expect(mockResponse.send).toHaveBeenCalled();
    });

    it("ユーザーが存在しない場合は400を返す", async () => {
      // Arrange
      const mockRequest = {
        params: { id: "999" },
      } as Request<UserIdParam>;

      const mockResponse = {
        status: vi.fn().mockReturnThis(),
        json: vi.fn(),
      } as unknown as Response;

      vi.mocked(mockDeleteUserUseCase.execute).mockRejectedValue(
        new Error("User not found")
      );

      // Act
      await controller.delete(mockRequest, mockResponse);

      // Assert
      expect(mockResponse.status).toHaveBeenCalledWith(400);
      expect(mockResponse.json).toHaveBeenCalledWith({
        error: "User not found",
      });
    });
  });
});

まとめ

今回は、プレゼンテーション層(コントローラー)のテストを実装しました。

これで、DDDアプリケーションの全層(ドメイン層、アプリケーション層、インフラ層、プレゼンテーション層)のテスト実装が完了しました。

  • ポイント
    • モックを活用: リクエスト、レスポンス、ユースケースをモック化
    • HTTPに焦点: ステータスコードとレスポンスボディを検証
    • エラーハンドリング: 正常系とエラー系の両方をテスト
    • メソッドチェーン: mockReturnThis()でExpressのメソッドチェーンを再現

全体のテスト戦略

テスト対象 モック対象 特徴
ドメイン層 値オブジェクト、エンティティ なし 最も独立、高速
アプリケーション層 ユースケース リポジトリ ビジネスロジック
インフラ層 リポジトリ なし(インメモリ) データ永続化
プレゼンテーション層 コントローラー ユースケース HTTP処理

DDDのテスト容易性

DDDで設計したアプリケーションは、以下の理由でテストが書きやすくなります。

  • 各層が独立している
  • 依存性注入により、モックに差し替えやすい
  • ビジネスロジックがドメイン層に集約されている
  • 外部依存が抽象化されている

テストを書くことで、リファクタリングや機能追加を安全に行えるようになります。

参考

1
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?