はじめに
この記事は、以下の記事の続きです。
今回は、プレゼンテーション層のテストを実装します。
開発環境
開発環境は以下の通りです。
- 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で設計したアプリケーションは、以下の理由でテストが書きやすくなります。
- 各層が独立している
- 依存性注入により、モックに差し替えやすい
- ビジネスロジックがドメイン層に集約されている
- 外部依存が抽象化されている
テストを書くことで、リファクタリングや機能追加を安全に行えるようになります。