はじめに
この記事は、以下の記事の続きです。
今回は、アプリケーション層のテストを実装します。
開発環境
開発環境は以下の通りです。
- 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で構造化されたテスト
- モックの検証: メソッドが正しく呼ばれたかを確認
- モックのメリット
- データベース不要で高速なテスト実行
- テストの独立性が保たれる
- エラーケースの再現が容易
- ビジネスロジックに集中できる
次回は、インフラ層のテストを実装します。インメモリリポジトリを使って、リポジトリの実装をテストします。