NestJSアプリケーションにおいて、ControllerはHTTPリクエストを受け取り、適切なServiceメソッドを呼び出してレスポンスを返却する役割を担います。Controller層のテストでは、リクエストハンドリングの正確性と、依存するServiceとの連携を検証します。本記事では、useValue・useClassによるモックプロバイダの注入から、jest.spyOn()を使用したServiceメソッドのモック化まで、実践的なControllerテスト手法を解説します。
実行環境と前提条件#
本記事の内容を実践するにあたり、以下の環境を前提としています。
| 項目 |
バージョン・要件 |
| Node.js |
20以上 |
| npm |
10以上 |
| NestJS |
11.x |
| @nestjs/testing |
11.x |
| Jest |
29.x |
| OS |
Windows / macOS / Linux |
| エディタ |
VS Code(推奨) |
事前に以下の準備を完了してください。
- NestJS CLIのインストール済み
- NestJSプロジェクトの作成済み
- Serviceのユニットテストに関する基礎知識
NestJSのユニットテスト基礎については前回の記事を参照してください。
ControllerテストとServiceテストの違い#
Serviceテストがビジネスロジックの正確性を検証するのに対し、Controllerテストは以下の観点を検証します。
Controllerテストの検証ポイント#
| 検証項目 |
説明 |
| ルーティング |
正しいエンドポイントにリクエストがルーティングされるか |
| パラメータ抽出 |
@Param、@Query、@Bodyで正しくデータが取得されるか |
| Serviceへの委譲 |
適切なServiceメソッドが正しい引数で呼び出されるか |
| レスポンス形式 |
期待される形式でレスポンスが返却されるか |
| エラーハンドリング |
例外が適切に処理されるか |
テスト戦略の選択#
Controllerのテストには2つのアプローチがあります。
flowchart TD
A[Controllerテスト戦略] --> B[ユニットテスト]
A --> C[E2Eテスト]
B --> D[Serviceをモック化]
B --> E[HTTP層を介さない]
B --> F[高速に実行可能]
C --> G[実際のHTTPリクエスト]
C --> H[統合的な動作確認]
C --> I[実行時間が長い]本記事では、ユニットテストアプローチに焦点を当てます。E2Eテストについては別記事で解説します。
テスト対象のController#
本記事で使用するサンプルControllerを定義します。
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
|
// users.controller.ts
import {
Controller,
Get,
Post,
Put,
Delete,
Param,
Body,
Query,
ParseIntPipe,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';
import { PaginationQueryDto } from './dto/pagination-query.dto';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
async findAll(@Query() paginationQuery: PaginationQueryDto): Promise<User[]> {
return this.usersService.findAll(paginationQuery);
}
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number): Promise<User> {
return this.usersService.findOne(id);
}
@Post()
async create(@Body() createUserDto: CreateUserDto): Promise<User> {
return this.usersService.create(createUserDto);
}
@Put(':id')
async update(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto,
): Promise<User> {
return this.usersService.update(id, updateUserDto);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id', ParseIntPipe) id: number): Promise<void> {
return this.usersService.remove(id);
}
}
|
対応するServiceインターフェースは以下のとおりです。
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
|
// users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { PaginationQueryDto } from './dto/pagination-query.dto';
@Injectable()
export class UsersService {
async findAll(paginationQuery: PaginationQueryDto): Promise<User[]> {
// 実装は省略
return [];
}
async findOne(id: number): Promise<User> {
// 実装は省略
throw new NotFoundException();
}
async create(createUserDto: CreateUserDto): Promise<User> {
// 実装は省略
return {} as User;
}
async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
// 実装は省略
return {} as User;
}
async remove(id: number): Promise<void> {
// 実装は省略
}
}
|
useValueによるモックプロバイダの注入#
最もシンプルなモック手法は、useValueパターンでオブジェクトリテラルを注入する方法です。
基本的なuseValueパターン#
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
|
// users.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
describe('UsersController', () => {
let controller: UsersController;
let service: UsersService;
// モックServiceの定義
const mockUsersService = {
findAll: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{
provide: UsersService,
useValue: mockUsersService,
},
],
}).compile();
controller = module.get<UsersController>(UsersController);
service = module.get<UsersService>(UsersService);
// 各テスト前にモックをリセット
jest.clearAllMocks();
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
|
useValueパターンの特徴は以下のとおりです。
| 特徴 |
説明 |
| シンプルな構文 |
オブジェクトリテラルで直接定義できる |
| 柔軟な戻り値設定 |
mockResolvedValue等で各テストごとに戻り値を変更可能 |
| 部分的なモック |
必要なメソッドのみ定義すればよい |
findAllメソッドのテスト#
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
|
describe('findAll', () => {
it('should return an array of users', async () => {
const expectedUsers: User[] = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
const paginationQuery: PaginationQueryDto = { limit: 10, offset: 0 };
mockUsersService.findAll.mockResolvedValue(expectedUsers);
const result = await controller.findAll(paginationQuery);
expect(result).toEqual(expectedUsers);
expect(mockUsersService.findAll).toHaveBeenCalledWith(paginationQuery);
expect(mockUsersService.findAll).toHaveBeenCalledTimes(1);
});
it('should return empty array when no users exist', async () => {
const paginationQuery: PaginationQueryDto = { limit: 10, offset: 0 };
mockUsersService.findAll.mockResolvedValue([]);
const result = await controller.findAll(paginationQuery);
expect(result).toEqual([]);
});
});
|
findOneメソッドのテスト#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
describe('findOne', () => {
it('should return a user when found', async () => {
const expectedUser: User = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
};
mockUsersService.findOne.mockResolvedValue(expectedUser);
const result = await controller.findOne(1);
expect(result).toEqual(expectedUser);
expect(mockUsersService.findOne).toHaveBeenCalledWith(1);
});
it('should propagate NotFoundException from service', async () => {
const { NotFoundException } = await import('@nestjs/common');
mockUsersService.findOne.mockRejectedValue(
new NotFoundException('User with ID 999 not found'),
);
await expect(controller.findOne(999)).rejects.toThrow(NotFoundException);
});
});
|
createメソッドのテスト#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
describe('create', () => {
it('should create and return a new user', async () => {
const createUserDto: CreateUserDto = {
name: 'Charlie',
email: 'charlie@example.com',
};
const expectedUser: User = { id: 3, ...createUserDto };
mockUsersService.create.mockResolvedValue(expectedUser);
const result = await controller.create(createUserDto);
expect(result).toEqual(expectedUser);
expect(mockUsersService.create).toHaveBeenCalledWith(createUserDto);
});
});
|
updateメソッドのテスト#
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
|
describe('update', () => {
it('should update and return the user', async () => {
const updateUserDto: UpdateUserDto = { name: 'Alice Updated' };
const expectedUser: User = {
id: 1,
name: 'Alice Updated',
email: 'alice@example.com',
};
mockUsersService.update.mockResolvedValue(expectedUser);
const result = await controller.update(1, updateUserDto);
expect(result).toEqual(expectedUser);
expect(mockUsersService.update).toHaveBeenCalledWith(1, updateUserDto);
});
it('should propagate NotFoundException when user not found', async () => {
const { NotFoundException } = await import('@nestjs/common');
const updateUserDto: UpdateUserDto = { name: 'Updated' };
mockUsersService.update.mockRejectedValue(
new NotFoundException('User with ID 999 not found'),
);
await expect(controller.update(999, updateUserDto)).rejects.toThrow(
NotFoundException,
);
});
});
|
removeメソッドのテスト#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
describe('remove', () => {
it('should remove the user', async () => {
mockUsersService.remove.mockResolvedValue(undefined);
await controller.remove(1);
expect(mockUsersService.remove).toHaveBeenCalledWith(1);
});
it('should propagate NotFoundException when user not found', async () => {
const { NotFoundException } = await import('@nestjs/common');
mockUsersService.remove.mockRejectedValue(
new NotFoundException('User with ID 999 not found'),
);
await expect(controller.remove(999)).rejects.toThrow(NotFoundException);
});
});
|
useClassによるモッククラスの注入#
より構造化されたモックが必要な場合は、useClassパターンでモッククラスを定義します。
モッククラスの定義#
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
|
// mocks/users.service.mock.ts
import { User } from '../entities/user.entity';
import { CreateUserDto } from '../dto/create-user.dto';
import { UpdateUserDto } from '../dto/update-user.dto';
import { PaginationQueryDto } from '../dto/pagination-query.dto';
export class MockUsersService {
private users: User[] = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
findAll = jest.fn().mockImplementation((query: PaginationQueryDto) => {
const { limit = 10, offset = 0 } = query;
return Promise.resolve(this.users.slice(offset, offset + limit));
});
findOne = jest.fn().mockImplementation((id: number) => {
const user = this.users.find((u) => u.id === id);
return Promise.resolve(user);
});
create = jest.fn().mockImplementation((dto: CreateUserDto) => {
const newUser: User = {
id: this.users.length + 1,
...dto,
};
return Promise.resolve(newUser);
});
update = jest.fn().mockImplementation((id: number, dto: UpdateUserDto) => {
const user = this.users.find((u) => u.id === id);
if (user) {
Object.assign(user, dto);
}
return Promise.resolve(user);
});
remove = jest.fn().mockImplementation((id: number) => {
return Promise.resolve();
});
}
|
useClassパターンでのテスト#
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
|
// users.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { MockUsersService } from './mocks/users.service.mock';
describe('UsersController with useClass', () => {
let controller: UsersController;
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{
provide: UsersService,
useClass: MockUsersService,
},
],
}).compile();
controller = module.get<UsersController>(UsersController);
service = module.get<UsersService>(UsersService);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('findAll', () => {
it('should return paginated users', async () => {
const result = await controller.findAll({ limit: 1, offset: 0 });
expect(result).toHaveLength(1);
expect(result[0].name).toBe('Alice');
});
});
});
|
useValueとuseClassの使い分け#
| パターン |
適したケース |
| useValue |
単純なモック、テストごとに戻り値を変更する場合 |
| useClass |
複雑なモックロジック、複数のテストファイルで共有する場合 |
jest.spyOn()によるServiceメソッドのモック化#
既存のServiceインスタンスに対してスパイを設定し、特定のメソッドだけをモック化する手法です。
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
describe('UsersController with spyOn', () => {
let controller: UsersController;
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [UsersService],
}).compile();
controller = module.get<UsersController>(UsersController);
service = module.get<UsersService>(UsersService);
});
describe('findAll', () => {
it('should call service.findAll with pagination query', async () => {
const expectedUsers: User[] = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
];
const paginationQuery: PaginationQueryDto = { limit: 10, offset: 0 };
// spyOnでメソッドをモック
jest.spyOn(service, 'findAll').mockResolvedValue(expectedUsers);
const result = await controller.findAll(paginationQuery);
expect(result).toEqual(expectedUsers);
expect(service.findAll).toHaveBeenCalledWith(paginationQuery);
});
});
describe('findOne', () => {
it('should call service.findOne with correct id', async () => {
const expectedUser: User = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
};
jest.spyOn(service, 'findOne').mockResolvedValue(expectedUser);
const result = await controller.findOne(1);
expect(result).toEqual(expectedUser);
expect(service.findOne).toHaveBeenCalledWith(1);
});
});
});
|
spyOnのメリットと注意点#
| メリット |
注意点 |
| 元の実装を保持しながら監視できる |
テスト後にスパイをリストアする必要がある場合がある |
| 特定のメソッドのみモック化できる |
beforeEachでService自体を取得する必要がある |
| 呼び出し回数・引数の検証が容易 |
モックの戻り値を毎回設定する必要がある |
mockImplementationによる詳細な制御#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
describe('create with mockImplementation', () => {
it('should transform input before creating', async () => {
const createUserDto: CreateUserDto = {
name: 'Charlie',
email: 'charlie@example.com',
};
jest.spyOn(service, 'create').mockImplementation(async (dto) => {
// モック内でロジックを実行可能
return {
id: 100,
name: dto.name.toUpperCase(),
email: dto.email,
};
});
const result = await controller.create(createUserDto);
expect(result.name).toBe('CHARLIE');
expect(result.id).toBe(100);
});
});
|
overrideProvider()による動的なモック設定#
TestingModuleBuilderのoverrideProvider()メソッドを使用すると、既存のモジュールをインポートしつつ、特定のProviderだけをオーバーライドできます。
overrideProviderの基本構文#
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
|
describe('UsersController with overrideProvider', () => {
let controller: UsersController;
beforeEach(async () => {
const mockService = {
findAll: jest.fn().mockResolvedValue([]),
findOne: jest.fn().mockResolvedValue(null),
create: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
imports: [UsersModule],
})
.overrideProvider(UsersService)
.useValue(mockService)
.compile();
controller = module.get<UsersController>(UsersController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
|
複数のProviderをオーバーライド#
1
2
3
4
5
6
7
8
9
10
|
const module: TestingModule = await Test.createTestingModule({
imports: [UsersModule],
})
.overrideProvider(UsersService)
.useValue(mockUsersService)
.overrideProvider(LoggerService)
.useValue(mockLoggerService)
.overrideProvider(ConfigService)
.useValue(mockConfigService)
.compile();
|
メソッドチェーンで複数のProviderを順次オーバーライドできます。
Guardをオーバーライドするテスト#
認証Guardが設定されたControllerをテストする場合、Guardのオーバーライドが必要です。
AuthGuardのオーバーライド#
1
2
3
4
5
6
|
// テスト対象のController
@Controller('users')
@UseGuards(AuthGuard)
export class UsersController {
// ...
}
|
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
|
// テストでのGuardオーバーライド
import { CanActivate, ExecutionContext } from '@nestjs/common';
describe('UsersController with Guard override', () => {
let controller: UsersController;
// 常にtrueを返すモックGuard
const mockAuthGuard: CanActivate = {
canActivate: (context: ExecutionContext) => true,
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{
provide: UsersService,
useValue: mockUsersService,
},
],
})
.overrideGuard(AuthGuard)
.useValue(mockAuthGuard)
.compile();
controller = module.get<UsersController>(UsersController);
});
it('should bypass authentication', async () => {
mockUsersService.findAll.mockResolvedValue([]);
const result = await controller.findAll({ limit: 10, offset: 0 });
expect(result).toEqual([]);
});
});
|
グローバルGuardのオーバーライド#
APP_GUARDで登録されたグローバルGuardをオーバーライドする場合は、特別な設定が必要です。
1
2
3
4
5
6
7
8
9
10
11
|
// app.module.tsでの登録
@Module({
providers: [
{
provide: APP_GUARD,
useExisting: JwtAuthGuard, // useExistingを使用
},
JwtAuthGuard,
],
})
export class AppModule {}
|
1
2
3
4
5
6
7
|
// テストでのオーバーライド
const module: TestingModule = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(JwtAuthGuard)
.useValue({ canActivate: () => true })
.compile();
|
useClassではなくuseExistingで登録されたGuardは、通常のProviderと同様にオーバーライドできます。
完全なテストファイルの例#
これまでの内容をまとめた完全なテストファイルを示します。
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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
|
// users.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { PaginationQueryDto } from './dto/pagination-query.dto';
describe('UsersController', () => {
let controller: UsersController;
let service: UsersService;
const mockUsersService = {
findAll: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{
provide: UsersService,
useValue: mockUsersService,
},
],
}).compile();
controller = module.get<UsersController>(UsersController);
service = module.get<UsersService>(UsersService);
jest.clearAllMocks();
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('findAll', () => {
it('should return an array of users', async () => {
const expectedUsers: User[] = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
const paginationQuery: PaginationQueryDto = { limit: 10, offset: 0 };
mockUsersService.findAll.mockResolvedValue(expectedUsers);
const result = await controller.findAll(paginationQuery);
expect(result).toEqual(expectedUsers);
expect(mockUsersService.findAll).toHaveBeenCalledWith(paginationQuery);
expect(mockUsersService.findAll).toHaveBeenCalledTimes(1);
});
it('should return empty array when no users exist', async () => {
const paginationQuery: PaginationQueryDto = { limit: 10, offset: 0 };
mockUsersService.findAll.mockResolvedValue([]);
const result = await controller.findAll(paginationQuery);
expect(result).toEqual([]);
});
});
describe('findOne', () => {
it('should return a user when found', async () => {
const expectedUser: User = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
};
mockUsersService.findOne.mockResolvedValue(expectedUser);
const result = await controller.findOne(1);
expect(result).toEqual(expectedUser);
expect(mockUsersService.findOne).toHaveBeenCalledWith(1);
});
it('should propagate NotFoundException from service', async () => {
mockUsersService.findOne.mockRejectedValue(
new NotFoundException('User with ID 999 not found'),
);
await expect(controller.findOne(999)).rejects.toThrow(NotFoundException);
await expect(controller.findOne(999)).rejects.toThrow(
'User with ID 999 not found',
);
});
});
describe('create', () => {
it('should create and return a new user', async () => {
const createUserDto: CreateUserDto = {
name: 'Charlie',
email: 'charlie@example.com',
};
const expectedUser: User = { id: 3, ...createUserDto };
mockUsersService.create.mockResolvedValue(expectedUser);
const result = await controller.create(createUserDto);
expect(result).toEqual(expectedUser);
expect(mockUsersService.create).toHaveBeenCalledWith(createUserDto);
});
});
describe('update', () => {
it('should update and return the user', async () => {
const updateUserDto: UpdateUserDto = { name: 'Alice Updated' };
const expectedUser: User = {
id: 1,
name: 'Alice Updated',
email: 'alice@example.com',
};
mockUsersService.update.mockResolvedValue(expectedUser);
const result = await controller.update(1, updateUserDto);
expect(result).toEqual(expectedUser);
expect(mockUsersService.update).toHaveBeenCalledWith(1, updateUserDto);
});
it('should propagate NotFoundException when user not found', async () => {
const updateUserDto: UpdateUserDto = { name: 'Updated' };
mockUsersService.update.mockRejectedValue(
new NotFoundException('User with ID 999 not found'),
);
await expect(controller.update(999, updateUserDto)).rejects.toThrow(
NotFoundException,
);
});
});
describe('remove', () => {
it('should remove the user', async () => {
mockUsersService.remove.mockResolvedValue(undefined);
await controller.remove(1);
expect(mockUsersService.remove).toHaveBeenCalledWith(1);
expect(mockUsersService.remove).toHaveBeenCalledTimes(1);
});
it('should propagate NotFoundException when user not found', async () => {
mockUsersService.remove.mockRejectedValue(
new NotFoundException('User with ID 999 not found'),
);
await expect(controller.remove(999)).rejects.toThrow(NotFoundException);
});
});
});
|
テスト実行と結果確認#
作成したテストを実行し、結果を確認します。
1
2
3
4
5
6
7
8
|
# テスト実行
npm test -- users.controller.spec.ts
# 詳細な出力
npm test -- users.controller.spec.ts --verbose
# カバレッジ付きで実行
npm run test:cov -- users.controller.spec.ts
|
期待される出力は以下のとおりです。
PASS src/users/users.controller.spec.ts
UsersController
✓ should be defined (10 ms)
findAll
✓ should return an array of users (5 ms)
✓ should return empty array when no users exist (2 ms)
findOne
✓ should return a user when found (3 ms)
✓ should propagate NotFoundException from service (4 ms)
create
✓ should create and return a new user (2 ms)
update
✓ should update and return the user (2 ms)
✓ should propagate NotFoundException when user not found (2 ms)
remove
✓ should remove the user (2 ms)
✓ should propagate NotFoundException when user not found (2 ms)
Test Suites: 1 passed, 1 total
Tests: 10 passed, 10 total
ベストプラクティス#
ControllerテストにおけるBest Practiceをまとめます。
テスト設計のポイント#
| ポイント |
説明 |
| Serviceへの委譲を検証する |
Controllerはルーティングと委譲のみを担当する設計を前提とする |
| 正常系と異常系を網羅する |
成功ケースだけでなく、例外発生時の動作も検証する |
| モックはテストごとにリセットする |
jest.clearAllMocks()で状態をクリアする |
| パラメータの検証を行う |
Serviceに正しい引数が渡されているか確認する |
避けるべきパターン#
| パターン |
問題点 |
改善策 |
| 実装の詳細をテストする |
リファクタリングでテストが壊れる |
公開APIの動作を検証する |
| 複数の責務を1テストで検証する |
失敗時の原因特定が困難 |
1テスト1アサーションを意識する |
| ハードコードされた期待値 |
保守性が低下する |
変数や定数で管理する |
まとめ#
本記事では、NestJSにおけるControllerテストの実践的な手法を解説しました。
useValueパターンによるオブジェクトリテラルでのモック注入
useClassパターンによるモッククラスの活用
jest.spyOn()による既存インスタンスのメソッドモック化
overrideProvider()による動的なProvider置換
- Guardのオーバーライドによる認証バイパス
ControllerテストはServiceテストと異なり、HTTPリクエストの受け取りからServiceへの委譲、レスポンスの返却までのフローを検証します。適切なモック戦略を選択し、Controllerの責務に焦点を当てたテストを記述することで、アプリケーションの品質と保守性を高めることができます。
次の記事では、E2Eテストによる統合的なAPIテスト手法を解説します。
参考リンク#