NestJSアプリケーションの品質を担保するうえで、ユニットテストは不可欠な要素です。@nestjs/testingパッケージは、NestJSの依存性注入システムを活用したテスト環境を提供し、モジュールやServiceのテストを効率的に記述できます。本記事では、Test.createTestingModule()によるテストモジュールの作成から、依存関係のモック化、Serviceクラスのテスト実装まで、実践的なユニットテスト手法を解説します。
実行環境と前提条件#
本記事の内容を実践するにあたり、以下の環境を前提としています。
| 項目 |
バージョン・要件 |
| Node.js |
20以上 |
| npm |
10以上 |
| NestJS |
11.x |
| @nestjs/testing |
11.x |
| Jest |
29.x |
| OS |
Windows / macOS / Linux |
| エディタ |
VS Code(推奨) |
事前に以下の準備を完了してください。
- NestJS CLIのインストール済み
- NestJSプロジェクトの作成済み
NestJSプロジェクトの作成方法はNestJS入門記事を参照してください。Module・Controller・Providerの基本はアーキテクチャ解説記事で確認できます。
NestJSにおけるユニットテストの位置づけ#
NestJSはテストを重視したフレームワークであり、プロジェクト生成時にJestがデフォルトで設定されています。ユニットテストでは、個々のコンポーネント(Service、Controller、Guardなど)を分離してテストし、ビジネスロジックの正確性を検証します。
テストの種類と対象#
NestJSアプリケーションで実施するテストは以下のように分類されます。
| テスト種類 |
対象 |
ファイル命名規則 |
| ユニットテスト |
Service、Provider、個別クラス |
*.spec.ts |
| インテグレーションテスト |
モジュール間連携 |
*.spec.ts |
| E2Eテスト |
APIエンドポイント全体 |
*.e2e-spec.ts |
本記事ではユニットテスト、特にServiceクラスのテストに焦点を当てます。
テストファイルの配置規則#
NestJSでは、テストファイルをテスト対象のファイルと同じディレクトリに配置することが推奨されています。
src/
├── users/
│ ├── users.module.ts
│ ├── users.controller.ts
│ ├── users.controller.spec.ts # Controllerのテスト
│ ├── users.service.ts
│ └── users.service.spec.ts # Serviceのテスト
この配置により、関連するコードとテストを容易に把握でき、保守性が向上します。
@nestjs/testingパッケージのセットアップ#
NestJS CLIで作成したプロジェクトには、@nestjs/testingパッケージがすでにインストールされています。手動でインストールする場合は以下のコマンドを実行します。
1
|
npm install --save-dev @nestjs/testing
|
package.jsonにテスト用のスクリプトが設定されていることを確認してください。
1
2
3
4
5
6
7
|
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage"
}
}
|
Test.createTestingModule()の基本#
@nestjs/testingパッケージの中核となるのがTestクラスです。Test.createTestingModule()メソッドは、テスト用のモジュールを作成し、NestJSの依存性注入システムをテスト環境で利用できるようにします。
テストモジュールの作成フロー#
テストモジュールの作成から使用までのフローを確認しましょう。
sequenceDiagram
participant T as テストコード
participant TM as TestingModule
participant M as Module
participant S as Service
T->>TM: Test.createTestingModule()
TM->>M: モジュール設定を受け取る
T->>TM: compile()
TM->>TM: 依存関係を解決
T->>TM: moduleRef.get(Service)
TM->>S: インスタンスを返却
T->>S: テスト実行最小限のテストモジュール#
以下は、Serviceをテストするための最小限のテストコードです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
beforeEach(async () => {
// テストモジュールを作成
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
}).compile();
// Serviceインスタンスを取得
service = module.get<UsersService>(UsersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
|
Test.createTestingModule()には、通常の@Module()デコレータと同じメタデータオブジェクトを渡します。compile()メソッドを呼び出すことで、モジュールが初期化され、依存関係が解決されます。
moduleRef.get()によるインスタンス取得#
compile()メソッドの戻り値であるTestingModuleは、モジュール内のProviderインスタンスを取得するためのget()メソッドを提供します。
get()メソッドの使い方#
1
2
3
4
5
|
// Serviceインスタンスの取得
const usersService = module.get<UsersService>(UsersService);
// トークンを使用した取得(カスタムプロバイダの場合)
const configService = module.get<ConfigService>('CONFIG_SERVICE');
|
get()メソッドはシングルトンインスタンスを返却します。リクエストスコープやトランジェントスコープのProviderを取得する場合は、resolve()メソッドを使用します。
resolve()メソッドによるスコープ付きProvider取得#
1
2
|
// トランジェントスコープのProvider取得
const requestService = await module.resolve<RequestScopedService>(RequestScopedService);
|
resolve()は非同期メソッドであり、呼び出すたびに新しいインスタンスを返します。これは、リクエストごとに異なるインスタンスが必要なスコープ付きProviderの動作を再現します。
依存関係を持つServiceのテスト#
実際のアプリケーションでは、Serviceは他のServiceやRepositoryに依存していることがほとんどです。ユニットテストでは、これらの依存関係をモック化して、テスト対象のServiceを分離します。
テスト対象のService例#
以下のような依存関係を持つUsersServiceをテストする場合を考えます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
// users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { UsersRepository } from './users.repository';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';
@Injectable()
export class UsersService {
constructor(private readonly usersRepository: UsersRepository) {}
async findAll(): Promise<User[]> {
return this.usersRepository.findAll();
}
async findOne(id: number): Promise<User> {
const user = await this.usersRepository.findById(id);
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
async create(createUserDto: CreateUserDto): Promise<User> {
return this.usersRepository.create(createUserDto);
}
async remove(id: number): Promise<void> {
const user = await this.findOne(id);
await this.usersRepository.delete(user.id);
}
}
|
依存関係のモック化#
UsersRepositoryをモック化してテストを記述します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
// users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersRepository } from './users.repository';
describe('UsersService', () => {
let service: UsersService;
let repository: UsersRepository;
// モックリポジトリの定義
const mockUsersRepository = {
findAll: jest.fn(),
findById: jest.fn(),
create: jest.fn(),
delete: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: UsersRepository,
useValue: mockUsersRepository,
},
],
}).compile();
service = module.get<UsersService>(UsersService);
repository = module.get<UsersRepository>(UsersRepository);
// 各テスト前にモックをリセット
jest.clearAllMocks();
});
describe('findAll', () => {
it('should return an array of users', async () => {
const expectedUsers = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
mockUsersRepository.findAll.mockResolvedValue(expectedUsers);
const result = await service.findAll();
expect(result).toEqual(expectedUsers);
expect(mockUsersRepository.findAll).toHaveBeenCalledTimes(1);
});
});
describe('findOne', () => {
it('should return a user when found', async () => {
const expectedUser = { id: 1, name: 'Alice', email: 'alice@example.com' };
mockUsersRepository.findById.mockResolvedValue(expectedUser);
const result = await service.findOne(1);
expect(result).toEqual(expectedUser);
expect(mockUsersRepository.findById).toHaveBeenCalledWith(1);
});
it('should throw NotFoundException when user not found', async () => {
mockUsersRepository.findById.mockResolvedValue(null);
await expect(service.findOne(999)).rejects.toThrow(NotFoundException);
await expect(service.findOne(999)).rejects.toThrow('User with ID 999 not found');
});
});
describe('create', () => {
it('should create and return a new user', async () => {
const createUserDto = { name: 'Charlie', email: 'charlie@example.com' };
const expectedUser = { id: 3, ...createUserDto };
mockUsersRepository.create.mockResolvedValue(expectedUser);
const result = await service.create(createUserDto);
expect(result).toEqual(expectedUser);
expect(mockUsersRepository.create).toHaveBeenCalledWith(createUserDto);
});
});
describe('remove', () => {
it('should delete a user when found', async () => {
const existingUser = { id: 1, name: 'Alice', email: 'alice@example.com' };
mockUsersRepository.findById.mockResolvedValue(existingUser);
mockUsersRepository.delete.mockResolvedValue(undefined);
await service.remove(1);
expect(mockUsersRepository.findById).toHaveBeenCalledWith(1);
expect(mockUsersRepository.delete).toHaveBeenCalledWith(1);
});
it('should throw NotFoundException when user to delete not found', async () => {
mockUsersRepository.findById.mockResolvedValue(null);
await expect(service.remove(999)).rejects.toThrow(NotFoundException);
});
});
});
|
モック化のパターンと使い分け#
NestJSのテストでは、依存関係をモック化するための複数のパターンがあります。状況に応じて適切なパターンを選択してください。
useValueパターン#
オブジェクトリテラルでモックを定義する最もシンプルなパターンです。
1
2
3
4
5
6
7
|
{
provide: UsersRepository,
useValue: {
findAll: jest.fn().mockResolvedValue([]),
findById: jest.fn(),
},
}
|
メソッドごとに戻り値を柔軟に設定できるため、多くの場面で使用されます。
useClassパターン#
モック用のクラスを定義して使用するパターンです。
1
2
3
4
5
6
7
8
9
10
11
12
|
class MockUsersRepository {
findAll = jest.fn().mockResolvedValue([]);
findById = jest.fn();
create = jest.fn();
delete = jest.fn();
}
// テストモジュール内で使用
{
provide: UsersRepository,
useClass: MockUsersRepository,
}
|
モックの実装が複雑な場合や、複数のテストファイルでモックを共有する場合に適しています。
useFactoryパターン#
ファクトリ関数でモックを動的に生成するパターンです。
1
2
3
4
5
6
7
|
{
provide: UsersRepository,
useFactory: () => ({
findAll: jest.fn().mockResolvedValue([]),
findById: jest.fn(),
}),
}
|
他のProviderに依存するモックを作成する場合に有用です。
jest.spyOn()によるメソッドのスパイ#
特定のメソッドの呼び出しを監視したり、一時的に戻り値を変更したりする場合はjest.spyOn()を使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
describe('UsersService with spyOn', () => {
let service: UsersService;
let repository: UsersRepository;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService, UsersRepository],
}).compile();
service = module.get<UsersService>(UsersService);
repository = module.get<UsersRepository>(UsersRepository);
});
it('should call repository.findAll', async () => {
const expectedUsers = [{ id: 1, name: 'Alice' }];
// メソッドをスパイして戻り値をモック
jest.spyOn(repository, 'findAll').mockResolvedValue(expectedUsers);
const result = await service.findAll();
expect(result).toEqual(expectedUsers);
expect(repository.findAll).toHaveBeenCalled();
});
});
|
jest.spyOn()は元のメソッド実装を保持しながら監視できるため、部分的なモックが必要な場合に便利です。
overrideProvider()による柔軟なモック設定#
TestingModuleBuilderのoverrideProvider()メソッドを使用すると、モジュールインポート後にProviderをオーバーライドできます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
describe('UsersService with overrideProvider', () => {
let service: UsersService;
beforeEach(async () => {
const mockRepository = {
findAll: jest.fn().mockResolvedValue([]),
findById: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
imports: [UsersModule],
})
.overrideProvider(UsersRepository)
.useValue(mockRepository)
.compile();
service = module.get<UsersService>(UsersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
|
このパターンは、既存のモジュールをインポートしつつ、特定のProviderだけをモックに置き換える場合に有効です。E2Eテストでデータベース接続をモック化する際によく使用されます。
useMocker()による自動モック生成#
大量の依存関係を持つServiceをテストする場合、useMocker()メソッドで自動的にモックを生成できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
import { ModuleMocker, MockFunctionMetadata } from 'jest-mock';
const moduleMocker = new ModuleMocker(global);
describe('UsersService with useMocker', () => {
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
})
.useMocker((token) => {
// 特定のProviderに対するカスタムモック
if (token === UsersRepository) {
return {
findAll: jest.fn().mockResolvedValue([]),
findById: jest.fn(),
create: jest.fn(),
delete: jest.fn(),
};
}
// その他のProviderは自動モック
if (typeof token === 'function') {
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
}
})
.compile();
service = module.get<UsersService>(UsersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
|
useMocker()は依存関係が多いモジュールのテストセットアップを大幅に簡略化します。
テストカバレッジの確認#
テストの網羅性を確認するために、カバレッジレポートを生成します。
実行後、コンソールにカバレッジサマリーが表示され、coverage/ディレクトリにHTMLレポートが生成されます。
----------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
users.service.ts | 100 | 100 | 100 | 100 |
----------------------|---------|----------|---------|---------|-------------------
カバレッジは目標値を設定し、CI/CDパイプラインで自動チェックすることを推奨します。jest.config.jsでしきい値を設定できます。
1
2
3
4
5
6
7
8
9
10
11
|
// jest.config.js
module.exports = {
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
|
ベストプラクティスとよくある間違い#
NestJSのユニットテストを効果的に実装するためのベストプラクティスと、避けるべき間違いを紹介します。
ベストプラクティス#
| 項目 |
説明 |
| テストの独立性を保つ |
各テストケースは他のテストに依存せず、単独で実行できる状態を維持する |
| モックをリセットする |
beforeEachでjest.clearAllMocks()を呼び出し、テスト間の影響を排除する |
| 境界値をテストする |
正常系だけでなく、エッジケースや異常系も網羅する |
| 意味のあるテスト名をつける |
何をテストしているか、期待される結果は何かを明確に記述する |
| Arrange-Act-Assertパターン |
テストコードを準備・実行・検証の3段階で構成する |
よくある間違い#
| 間違い |
問題点 |
改善策 |
| 実装の詳細をテストする |
リファクタリングでテストが壊れやすくなる |
公開APIの動作をテストする |
| モックしすぎる |
テストが実装と乖離する |
適切な粒度でモックを使用する |
| 非同期処理の待機忘れ |
テストが不安定になる |
async/awaitを正しく使用する |
テスト実行とデバッグ#
テストの実行にはいくつかのオプションがあります。
1
2
3
4
5
6
7
8
9
10
11
|
# すべてのテストを実行
npm test
# ウォッチモードで実行(ファイル変更時に自動再実行)
npm run test:watch
# 特定のファイルのみ実行
npm test -- users.service.spec.ts
# 特定のテストケースのみ実行
npm test -- --testNamePattern="should return an array of users"
|
VS Codeを使用している場合、Jest拡張機能をインストールすることで、エディタ上から個別のテストを実行・デバッグできます。
まとめ#
本記事では、NestJSにおけるユニットテストの基礎を解説しました。
@nestjs/testingパッケージとTest.createTestingModule()によるテストモジュールの作成
moduleRef.get()とmoduleRef.resolve()によるインスタンス取得
useValue、useClass、useFactoryパターンによる依存関係のモック化
jest.spyOn()によるメソッドの監視とモック
overrideProvider()とuseMocker()による柔軟なモック設定
ユニットテストは、コードの品質を維持し、リファクタリングを安全に行うための基盤です。テスト駆動開発(TDD)の実践や、CI/CDパイプラインへの統合を通じて、継続的にテストを活用することで、堅牢なNestJSアプリケーションを構築できます。
次の記事では、Controllerのテストとモック手法について詳しく解説します。
参考リンク#