覆盖文档:OpenAPI(8 页全部), Testing(Fundamentals), Recipes(REPL, CRUD Generator, Suites, Compodoc) 前置知识:第 9 课(DI 深入、动态模块、ModuleRef) 源码重点:
packages/testing/testing-module.builder.ts,packages/testing/testing-module.ts
一、OpenAPI / Swagger
[基础] 本节面向初学者,掌握 API 文档的自动生成。
1.1 为什么需要 API 文档
前端开发者 后端开发者
│ │
│ "这个接口参数是什么?返回什么格式?" │
│ ─────────────────────────────────→ │
│ │
│ ← 写文档? 口头沟通? 过时了? │
│ │
↓ 解决方案
┌──────────────────────────────────────────────┐
│ OpenAPI / Swagger 自动文档 │
│ │
│ 代码即文档 → 装饰器标注 → 自动生成 → 实时同步 │
│ │
│ /api → 交互式文档页面 │
│ /api-json → JSON Schema │
│ /api-yaml → YAML Schema │
└──────────────────────────────────────────────┘
1.2 安装与基础配置
npm install --save @nestjs/swagger
在 main.ts 中配置 Swagger:
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 1. 构建文档配置
const config = new DocumentBuilder()
.setTitle('Cats API') // API 标题
.setDescription('The cats API description') // API 描述
.setVersion('1.0') // API 版本
.addBearerAuth() // 添加 Bearer Token 认证
.build();
// 2. 创建文档对象
const document = SwaggerModule.createDocument(app, config);
// 3. 挂载 Swagger UI
SwaggerModule.setup('api', app, document);
await app.listen(3000);
}
bootstrap();
启动后访问:
http://localhost:3000/api— 交互式 Swagger UI 页面http://localhost:3000/api-json— OpenAPI JSON Schemahttp://localhost:3000/api-yaml— OpenAPI YAML Schema
1.3 @ApiProperty() — DTO 属性标注
Swagger 需要知道 DTO 的属性类型和描述。使用 @ApiProperty() 装饰器标注:
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateCatDto {
@ApiProperty({
description: '猫的名字',
example: 'Tom',
minLength: 1,
maxLength: 50,
})
name: string;
@ApiProperty({
description: '猫的年龄',
minimum: 0,
maximum: 30,
example: 3,
})
age: number;
@ApiProperty({
description: '猫的品种',
enum: ['Persian', 'Siamese', 'Maine Coon'],
example: 'Persian',
})
breed: string;
@ApiPropertyOptional({
description: '猫的标签',
type: [String],
example: ['cute', 'fluffy'],
})
tags?: string[];
}
@ApiProperty() 常用选项:
| 选项 | 类型 | 说明 |
|---|---|---|
description | string | 属性描述 |
example | any | 示例值 |
required | boolean | 是否必填(默认 true) |
type | Type | 类型(复杂类型时必须指定) |
enum | any[] | 枚举值列表 |
default | any | 默认值 |
minimum / maximum | number | 数值范围 |
minLength / maxLength | number | 字符串长度范围 |
isArray | boolean | 是否为数组 |
nullable | boolean | 是否可为 null |
1.4 @ApiTags() — 控制器分组
使用 @ApiTags() 将控制器归类,在 Swagger UI 中分组显示:
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
@ApiTags('cats') // 归入 "cats" 分组
@Controller('cats')
export class CatsController {
@ApiOperation({ summary: '获取所有猫' })
@Get()
findAll() {
return [];
}
@ApiOperation({ summary: '创建一只猫' })
@Post()
create(@Body() createCatDto: CreateCatDto) {
return createCatDto;
}
}
1.5 响应装饰器
NestJS/Swagger 提供了 25+ 个响应装饰器,对应不同 HTTP 状态码:
import {
ApiOkResponse,
ApiCreatedResponse,
ApiNoContentResponse,
ApiBadRequestResponse,
ApiUnauthorizedResponse,
ApiForbiddenResponse,
ApiNotFoundResponse,
ApiConflictResponse,
ApiTooManyRequestsResponse,
ApiInternalServerErrorResponse,
} from '@nestjs/swagger';
@Controller('cats')
export class CatsController {
@Get()
@ApiOkResponse({
description: '成功返回猫列表',
type: [Cat], // 指定响应类型
})
@ApiForbiddenResponse({ description: '没有权限' })
findAll(): Cat[] {
return [];
}
@Post()
@ApiCreatedResponse({
description: '成功创建猫',
type: Cat,
})
@ApiBadRequestResponse({ description: '请求参数无效' })
@ApiConflictResponse({ description: '猫已存在' })
create(@Body() dto: CreateCatDto): Cat {
return {} as Cat;
}
@Delete(':id')
@ApiNoContentResponse({ description: '删除成功' })
@ApiNotFoundResponse({ description: '猫不存在' })
remove(@Param('id') id: string) {
return;
}
}
常用响应装饰器速查:
| 装饰器 | HTTP 状态码 | 语义 |
|---|---|---|
@ApiOkResponse() | 200 | 成功 |
@ApiCreatedResponse() | 201 | 创建成功 |
@ApiAcceptedResponse() | 202 | 已接受(异步) |
@ApiNoContentResponse() | 204 | 无内容 |
@ApiBadRequestResponse() | 400 | 请求无效 |
@ApiUnauthorizedResponse() | 401 | 未认证 |
@ApiForbiddenResponse() | 403 | 无权限 |
@ApiNotFoundResponse() | 404 | 未找到 |
@ApiConflictResponse() | 409 | 冲突 |
@ApiUnprocessableEntityResponse() | 422 | 参数验证失败 |
@ApiTooManyRequestsResponse() | 429 | 限流 |
@ApiInternalServerErrorResponse() | 500 | 服务器内部错误 |
二、OpenAPI 进阶
[中阶] 本节面向有经验的开发者,掌握 Swagger 高级配置。
2.1 CLI Plugin 自动推断
手动为每个 DTO 属性添加 @ApiProperty() 非常繁琐。CLI Plugin 可以在编译时自动推断:
在 nest-cli.json 中配置:
{
"compilerOptions": {
"plugins": [
{
"name": "@nestjs/swagger",
"options": {
"classValidatorShim": true, // 从 class-validator 推断
"introspectComments": true, // 从 JSDoc 注释推断描述
"dtoFileNameSuffix": [".dto.ts", ".entity.ts"]
}
}
]
}
}
启用后,以下 DTO 无需手动标注:
// 编译前 — 开发者只需写普通 DTO
export class CreateCatDto {
/** 猫的名字 */
name: string;
/** 猫的年龄 */
age: number;
/** 猫的品种 */
breed?: string; // 可选属性自动推断为 required: false
}
// 编译后 — Plugin 自动添加 @ApiProperty()
// @ApiProperty({ description: '猫的名字', required: true, type: String })
// @ApiProperty({ description: '猫的年龄', required: true, type: Number })
// @ApiProperty({ description: '猫的品种', required: false, type: String })
注意:启用 CLI Plugin 后,需要重新构建(
nest build)才能生效。使用 SWC 编译时不支持此 Plugin。
2.2 安全方案
DocumentBuilder 支持多种认证方案:
const config = new DocumentBuilder()
.setTitle('API')
// Bearer Token(JWT)
.addBearerAuth(
{ type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
'access-token', // 安全方案名称
)
// OAuth2
.addOAuth2({
type: 'oauth2',
flows: {
implicit: {
authorizationUrl: 'https://auth.example.com/authorize',
scopes: { 'read:cats': '读取猫', 'write:cats': '写入猫' },
},
},
})
// Cookie 认证
.addCookieAuth('session-id')
// API Key
.addApiKey({ type: 'apiKey', name: 'X-API-KEY', in: 'header' }, 'api-key')
.build();
在 Controller 中标注需要的认证方案:
@ApiBearerAuth('access-token') // 对应 DocumentBuilder 中的方案名称
@ApiOAuth2(['read:cats'])
@ApiCookieAuth('session-id')
@Controller('cats')
export class CatsController {}
2.3 映射类型
从 @nestjs/swagger 导入映射类型(而非 @nestjs/mapped-types),以保留 API 元数据:
import { PartialType, PickType, OmitType, IntersectionType } from '@nestjs/swagger';
// PartialType — 所有属性变为可选
export class UpdateCatDto extends PartialType(CreateCatDto) {}
// PickType — 选取部分属性
export class CatNameDto extends PickType(CreateCatDto, ['name'] as const) {}
// OmitType — 排除部分属性
export class CatWithoutBreedDto extends OmitType(CreateCatDto, ['breed'] as const) {}
// IntersectionType — 合并两个 DTO
export class FullCatDto extends IntersectionType(CreateCatDto, AdditionalCatInfo) {}
关键区别:从
@nestjs/swagger导入的映射类型会自动复制@ApiProperty()元数据;从@nestjs/mapped-types导入的不会。
2.4 多 Spec 端点
为不同模块生成独立的 API 文档:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 用户模块文档
const userConfig = new DocumentBuilder()
.setTitle('Users API')
.setVersion('1.0')
.build();
const userDocument = SwaggerModule.createDocument(app, userConfig, {
include: [UsersModule], // 只包含 UsersModule
});
SwaggerModule.setup('api/users', app, userDocument);
// 管理后台文档
const adminConfig = new DocumentBuilder()
.setTitle('Admin API')
.setVersion('1.0')
.addBearerAuth()
.build();
const adminDocument = SwaggerModule.createDocument(app, adminConfig, {
include: [AdminModule, UsersModule],
});
SwaggerModule.setup('api/admin', app, adminDocument);
await app.listen(3000);
}
2.5 文件上传文档
import { ApiConsumes, ApiBody } from '@nestjs/swagger';
@Controller('cats')
export class CatsController {
@Post('avatar')
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
properties: {
file: {
type: 'string',
format: 'binary',
},
description: {
type: 'string',
},
},
},
})
@UseInterceptors(FileInterceptor('file'))
uploadAvatar(@UploadedFile() file: Express.Multer.File) {
return { filename: file.originalname };
}
}
2.6 Schema 组合
使用 oneOf、anyOf、allOf 描述复杂类型关系:
import { ApiExtraModels, getSchemaPath } from '@nestjs/swagger';
@ApiExtraModels(Cat, Dog) // 注册额外的 Schema
@Controller('pets')
export class PetsController {
@Get()
@ApiOkResponse({
schema: {
oneOf: [ // 返回 Cat 或 Dog
{ $ref: getSchemaPath(Cat) },
{ $ref: getSchemaPath(Dog) },
],
},
})
findOne() {}
@Get('all')
@ApiOkResponse({
schema: {
type: 'array',
items: {
anyOf: [
{ $ref: getSchemaPath(Cat) },
{ $ref: getSchemaPath(Dog) },
],
},
},
})
findAll() {}
}
2.7 全局参数与响应
const config = new DocumentBuilder()
.setTitle('API')
.addGlobalParameters({
name: 'X-Request-ID',
in: 'header',
required: false,
description: '请求追踪 ID',
schema: { type: 'string' },
})
.build();
三、单元测试
[基础] 本节面向初学者,掌握 NestJS 的测试基础。
3.1 测试架构
┌─────────────────────────────────────────────────────┐
│ 测试金字塔 │
│ │
│ ╱╲ │
│ ╱E2E╲ ← 少量(10%) │
│ ╱──────╲ 完整应用 + HTTP 请求 │
│ ╱ 集成测试 ╲ ← 适量(20%) │
│ ╱────────────╲ 多模块协作 │
│ ╱ 单元测试 ╲ ← 大量(70%) │
│ ╱────────────────╲ 单个类/方法 │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────┘
3.2 @nestjs/testing 基础
NestJS 提供 @nestjs/testing 包创建测试模块,模拟完整的 DI 容器:
import { Test, TestingModule } from '@nestjs/testing';
import { CatsService } from './cats.service';
import { CatsRepository } from './cats.repository';
describe('CatsService', () => {
let service: CatsService;
let repository: CatsRepository;
// 模拟 Repository
const mockRepository = {
findAll: jest.fn().mockResolvedValue([{ id: 1, name: 'Tom', age: 3 }]),
findOne: jest.fn().mockResolvedValue({ id: 1, name: 'Tom', age: 3 }),
create: jest.fn().mockResolvedValue({ id: 2, name: 'Jerry', age: 1 }),
remove: jest.fn().mockResolvedValue(undefined),
};
beforeEach(async () => {
// 1. 创建测试模块
const module: TestingModule = await Test.createTestingModule({
providers: [
CatsService,
{
provide: CatsRepository,
useValue: mockRepository, // mock 掉 Repository
},
],
}).compile();
// 2. 获取待测实例
service = module.get<CatsService>(CatsService);
repository = module.get<CatsRepository>(CatsRepository);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('findAll', () => {
it('should return an array of cats', async () => {
const result = await service.findAll();
expect(result).toEqual([{ id: 1, name: 'Tom', age: 3 }]);
expect(mockRepository.findAll).toHaveBeenCalled();
});
});
describe('findOne', () => {
it('should return a single cat', async () => {
const result = await service.findOne(1);
expect(result).toEqual({ id: 1, name: 'Tom', age: 3 });
expect(mockRepository.findOne).toHaveBeenCalledWith(1);
});
});
describe('create', () => {
it('should create a new cat', async () => {
const dto = { name: 'Jerry', age: 1, breed: 'Siamese' };
const result = await service.create(dto);
expect(result).toEqual({ id: 2, name: 'Jerry', age: 1 });
expect(mockRepository.create).toHaveBeenCalledWith(dto);
});
});
});
3.3 overrideProvider — 替换依赖
const module = await Test.createTestingModule({
imports: [CatsModule], // 导入真实模块
}).overrideProvider(CatsRepository) // 替换指定 Provider
.useValue(mockRepository) // 使用 mock 值
.compile();
overrideProvider 支持三种替换方式:
| 方法 | 说明 | 示例 |
|---|---|---|
.useValue(mock) | 使用具体值 | useValue({ findAll: jest.fn() }) |
.useClass(MockClass) | 使用替代类 | useClass(MockCatsRepository) |
.useFactory({ factory }) | 使用工厂函数 | useFactory({ factory: () => new Mock() }) |
3.4 useMocker — 自动 Mock
useMocker() 可以自动为所有未提供的依赖生成 mock:
import { ModuleMetadata } from '@nestjs/common';
import { MockFunctionMetadata, ModuleMocker } from 'jest-mock';
const moduleMocker = new ModuleMocker(global);
const module = await Test.createTestingModule({
providers: [CatsService],
// 不需要手动提供 CatsRepository 等依赖
})
.useMocker((token) => {
if (token === CatsRepository) {
// 自定义特定 mock
return {
findAll: jest.fn().mockResolvedValue([]),
};
}
if (typeof token === 'function') {
// 自动生成 mock
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
}
})
.compile();
3.5 Jest 常用断言
// 值断言
expect(result).toBe(42); // 严格相等
expect(result).toEqual({ name: 'Tom' }); // 深度相等
expect(result).toBeDefined(); // 不为 undefined
expect(result).toBeNull(); // 为 null
expect(result).toBeTruthy(); // 真值
expect(result).toContain('Tom'); // 包含元素
// 函数调用断言
expect(mockFn).toHaveBeenCalled(); // 被调用过
expect(mockFn).toHaveBeenCalledTimes(1); // 调用次数
expect(mockFn).toHaveBeenCalledWith('arg1'); // 调用参数
// 异步断言
await expect(service.findOne(999)).rejects.toThrow(NotFoundException);
// Spy
const spy = jest.spyOn(service, 'findAll');
await controller.findAll();
expect(spy).toHaveBeenCalled();
四、E2E 测试与高级 Mock
[中阶] 本节面向有经验的开发者,掌握端到端测试与高级模拟技术。
4.1 E2E 测试基础
E2E 测试创建完整的 NestJS 应用实例,通过 HTTP 请求验证整个请求管线:
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('CatsController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
// 创建完整的 NestJS 应用
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
it('/cats (GET)', () => {
return request(app.getHttpServer())
.get('/cats')
.expect(200)
.expect((res) => {
expect(Array.isArray(res.body)).toBe(true);
});
});
it('/cats (POST)', () => {
return request(app.getHttpServer())
.post('/cats')
.send({ name: 'Tom', age: 3, breed: 'Persian' })
.expect(201)
.expect((res) => {
expect(res.body.name).toBe('Tom');
});
});
it('/cats/:id (GET) - not found', () => {
return request(app.getHttpServer())
.get('/cats/999')
.expect(404);
});
});
4.2 overrideGuard — 跳过认证
E2E 测试中常需跳过认证 Guard:
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
})
.overrideGuard(AuthGuard)
.useValue({ canActivate: () => true }) // 始终放行
.compile();
4.3 overrideModule — 替换整个模块
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
})
.overrideModule(ConfigModule)
.useModule(TestConfigModule) // 使用测试专用配置模块
.compile();
4.4 Suites(Automock)— 自动化 Mock
@automock/jest 提供 TestBed.solitary() 方法,自动为所有依赖生成类型安全的 mock:
npm install --save-dev @automock/jest @automock/adapters.nestjs
import { TestBed } from '@automock/jest';
import { CatsService } from './cats.service';
import { CatsRepository } from './cats.repository';
describe('CatsService (Automock)', () => {
let service: CatsService;
let repository: jest.Mocked<CatsRepository>;
beforeEach(() => {
// 自动 mock 所有依赖
const { unit, unitRef } = TestBed.create(CatsService).compile();
service = unit;
repository = unitRef.get(CatsRepository); // 类型安全的 mock
});
it('should call repository.findAll', async () => {
repository.findAll.mockResolvedValue([]); // IDE 自动补全
const result = await service.findAll();
expect(result).toEqual([]);
expect(repository.findAll).toHaveBeenCalled();
});
});
4.5 REPL 模式
REPL(Read-Eval-Print Loop)模式允许在终端交互式探索 DI 容器:
// repl.ts
import { repl } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
await repl(AppModule);
}
bootstrap();
# 启动 REPL
npm run start -- --entryFile repl
在 REPL 中交互式调试:
> get(CatsService).findAll()
> get(CatsService).create({ name: 'Tom', age: 3 })
> debug(CatsModule)
> methods(CatsService)
REPL 常用函数:
| 函数 | 说明 |
|---|---|
get(token) | 获取 Provider 实例 |
resolve(token) | 解析 Scoped Provider |
select(module).get(token) | 从指定模块获取 Provider |
debug(module?) | 打印模块的依赖图 |
methods(instance) | 列出实例的所有方法 |
五、测试进阶
[高阶] 本节面向深入使用者,掌握复杂场景的测试技巧。
5.1 Request-scoped Provider 测试
Request-scoped Provider 每个请求创建新实例,测试时需要手动创建上下文:
import { ContextIdFactory } from '@nestjs/core';
describe('RequestScopedService', () => {
let app: INestApplication;
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = module.createNestApplication();
await app.init();
});
it('should resolve request-scoped provider', async () => {
// 创建请求上下文
const contextId = ContextIdFactory.create();
// 模拟请求对象
jest.spyOn(ContextIdFactory, 'getByRequest').mockImplementation(() => contextId);
// 在特定上下文中解析 Provider
const service = await app.resolve(RequestScopedService, contextId);
expect(service).toBeDefined();
expect(service).toBeInstanceOf(RequestScopedService);
});
});
5.2 微服务测试
import { Transport } from '@nestjs/microservices';
describe('Microservice (e2e)', () => {
let app: INestMicroservice;
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = module.createNestMicroservice({
transport: Transport.TCP,
options: { port: 3001 },
});
await app.listen();
});
afterAll(async () => {
await app.close();
});
});
5.3 全局 Guard 测试技巧
使用 useExisting 使全局 Guard 可被 override:
// app.module.ts — 注册全局 Guard
@Module({
providers: [
AuthGuard, // 先注册为普通 Provider
{
provide: APP_GUARD,
useExisting: AuthGuard, // 用 useExisting 引用(而非 useClass)
},
],
})
export class AppModule {}
// 测试中可以 override
const module = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(AuthGuard) // 因为 useExisting,override 生效
.useValue({ canActivate: () => true })
.compile();
5.4 setLogger — 自定义测试日志
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
const app = module.createNestApplication();
// 禁用测试中的日志输出
app.useLogger(false);
// 或使用自定义日志器捕获日志
const logs: string[] = [];
app.useLogger({
log: (msg) => logs.push(msg),
error: (msg) => logs.push(msg),
warn: (msg) => logs.push(msg),
debug: (msg) => logs.push(msg),
verbose: (msg) => logs.push(msg),
});
5.5 Compodoc — 项目文档生成
Compodoc 为 NestJS 项目生成详细的代码文档:
# 安装
npm install --save-dev @compodoc/compodoc
# 生成文档
npx @compodoc/compodoc -p tsconfig.json -s
# 生成并启动文档服务器
npx @compodoc/compodoc -p tsconfig.json -s --port 8080
生成的文档包含:模块依赖图、控制器详情、服务方法列表、覆盖率报告。
5.6 CRUD Generator
nest g resource 一键生成完整 CRUD 骨架,包含测试文件:
nest g resource cats
# 选择 REST API → 自动生成:
# cats.controller.ts + cats.controller.spec.ts
# cats.service.ts + cats.service.spec.ts
# cats.module.ts
# dto/create-cat.dto.ts + dto/update-cat.dto.ts
# entities/cat.entity.ts
生成的 Service 测试骨架:
import { Test, TestingModule } from '@nestjs/testing';
import { CatsService } from './cats.service';
describe('CatsService', () => {
let service: CatsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CatsService],
}).compile();
service = module.get<CatsService>(CatsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
六、源码解读:测试模块
[资深] 本节面向希望深入理解框架内部机制的读者。
6.1 TestingModuleBuilder — Override 链
文件位置:packages/testing/testing-module.builder.ts
export class TestingModuleBuilder {
private readonly overloadsMap = new Map();
// override 链 — 每个方法返回 this 实现链式调用
overrideProvider(typeOrToken: any): OverrideBy {
return this.override(typeOrToken, this.overloadsMap);
}
overrideGuard(typeOrToken: any): OverrideBy {
return this.override(typeOrToken, this.guardsOverloadsMap);
}
overrideInterceptor(typeOrToken: any): OverrideBy {
return this.override(typeOrToken, this.interceptorsOverloadsMap);
}
overrideFilter(typeOrToken: any): OverrideBy {
return this.override(typeOrToken, this.filtersOverloadsMap);
}
overridePipe(typeOrToken: any): OverrideBy {
return this.override(typeOrToken, this.pipesOverloadsMap);
}
overrideModule(moduleToOverride: any): OverrideByModule {
return {
useModule: (newModule: any) => {
this.moduleOverloadsMap.set(moduleToOverride, newModule);
return this;
},
};
}
// compile() 构建最终模块
async compile(): Promise<TestingModule> {
// 1. 扫描模块树(同正式应用)
// 2. 应用所有 override
// 3. 实例化依赖
// 4. 返回 TestingModule
}
}
Override 链的设计使得测试代码非常优雅:
await Test.createTestingModule({ imports: [AppModule] })
.overrideProvider(DatabaseService).useValue(mockDb)
.overrideGuard(AuthGuard).useValue({ canActivate: true })
.overrideInterceptor(LoggingInterceptor).useValue({})
.compile();
6.2 TestingModule — 扩展 NestApplicationContext
文件位置:packages/testing/testing-module.ts
export class TestingModule extends NestApplicationContext {
// 复用了完整的 DI 容器能力
// 继承:get(), resolve(), select() 等方法
// 创建完整 HTTP 应用(用于 E2E 测试)
createNestApplication(httpAdapter?: AbstractHttpAdapter): INestApplication {
// 创建的应用与真实应用几乎完全相同
// 但使用了 override 后的 DI 容器
}
// 创建微服务应用
createNestMicroservice(options: MicroserviceOptions): INestMicroservice {
// 类似 createNestApplication,但用于微服务
}
}
核心设计:TestingModule 继承了 NestApplicationContext,这意味着测试中的 DI 容器与生产环境使用完全相同的代码路径,只是通过 override 替换了特定 Provider。
6.3 useMocker 实现原理
// 简化版 useMocker 实现
useMocker(mocker: MockFunction): this {
this.mocker = mocker;
return this;
}
// 在依赖解析阶段
private applyMocker(token: any): any {
if (this.mocker && !this.hasProvider(token)) {
// 依赖未找到时,调用 mocker 生成 mock
const mockInstance = this.mocker(token);
if (mockInstance) {
return mockInstance;
}
}
throw new UnknownDependencyException();
}
useMocker 的工作原理:当 DI 容器尝试解析一个未注册的依赖时,不是抛出异常,而是调用 mocker 函数生成一个 mock 实例。
七、质量保障策略
[架构] 本节面向技术负责人和架构师。
7.1 API 文档即合约
┌─────────────────────────────────────────────────────┐
│ API 文档作为 Single Source of Truth │
│ │
│ @ApiProperty() ─→ Swagger UI ─→ 前端团队参考 │
│ │ │
│ ├─→ /api-json ─→ 代码生成 │
│ │ (openapi-generator) │
│ │ │
│ └─→ 契约测试 ─→ CI/CD 验证 │
│ (Pact / Dredd) │
└─────────────────────────────────────────────────────┘
核心原则:
- 代码即文档:通过装饰器在代码中定义文档,保证文档与实现同步
- 自动化验证:在 CI 中验证 API 是否符合 OpenAPI Schema
- 客户端代码生成:从 OpenAPI Schema 自动生成前端 API 客户端
7.2 测试金字塔实践
| 层级 | 占比 | 测试范围 | 工具 | 运行速度 |
|---|---|---|---|---|
| 单元测试 | 70% | 单个 Service/Pipe/Guard | @nestjs/testing + Jest | 毫秒级 |
| 集成测试 | 20% | 模块间协作、数据库交互 | TestingModule + 真实 DB | 秒级 |
| E2E 测试 | 10% | 完整 HTTP 请求管线 | supertest + 完整应用 | 秒级 |
7.3 测试覆盖率目标
# 运行测试并生成覆盖率报告
npx jest --coverage
# 覆盖率阈值配置(jest.config.ts)
module.exports = {
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
目标:80%+ 代码覆盖率,重点覆盖:
- 业务逻辑(Service 层)
- 数据转换(Pipe、Interceptor)
- 权限控制(Guard)
- 错误处理(ExceptionFilter)
7.4 契约测试
微服务架构中,各服务间的接口必须兼容。契约测试确保:
┌──────────┐ ┌──────────┐
│ 服务 A │ ─── 契约(Schema)──→ │ 服务 B │
│ (消费者) │ │ (提供者) │
└──────────┘ └──────────┘
│ │
└── 消费者测试: └── 提供者测试:
"我期望收到什么格式?" "我是否满足消费者的期望?"
工具:Pact、Dredd、OpenAPI diff
7.5 质量保障决策清单
| 决策点 | 推荐方案 | 备选方案 |
|---|---|---|
| API 文档 | @nestjs/swagger(自动) | 手动维护 Markdown |
| 单元测试框架 | Jest(默认) | Vitest |
| Mock 策略 | overrideProvider + useMocker | Automock(@automock/jest) |
| E2E 测试 | supertest | Pactum |
| 覆盖率 | 80%+ 全局阈值 | 按模块设置不同阈值 |
| 契约测试 | OpenAPI Schema diff | Pact |
| 文档生成 | Swagger UI + Compodoc | Swagger UI only |
八、课后实践
练习 1:添加 Swagger 文档(基础)
为一个 CRUD 模块添加完整的 API 文档:
// 1. 在 main.ts 配置 SwaggerModule
// 2. 为 CreateCatDto 添加 @ApiProperty()
// 3. 为 Controller 添加 @ApiTags() 和 @ApiOperation()
// 4. 为每个方法添加响应装饰器
// 5. 访问 /api 验证文档
练习 2:编写单元测试(基础)
为 CatsService 编写完整的单元测试,覆盖以下场景:
describe('CatsService', () => {
// mock CatsRepository
// 测试 findAll() — 正常返回列表
// 测试 findOne() — 找到 → 返回 Cat
// 测试 findOne() — 未找到 → 抛出 NotFoundException
// 测试 create() — 正常创建
// 测试 remove() — 正常删除
// 测试 remove() — 未找到 → 抛出 NotFoundException
});
练习 3:编写 E2E 测试(中阶)
// 1. 创建测试模块,override AuthGuard
// 2. 测试完整 CRUD 流程:
// POST /cats → 201
// GET /cats → 200(包含刚创建的 cat)
// GET /cats/:id → 200
// PATCH /cats/:id → 200
// DELETE /cats/:id → 200
// GET /cats/:id → 404(已删除)
练习 4:使用 REPL 调试(中阶)
# 1. 创建 repl.ts 入口文件
# 2. 启动 REPL 模式
# 3. 尝试以下命令:
# get(CatsService).findAll()
# debug(CatsModule)
# methods(CatsService)
练习 5:阅读 TestingModuleBuilder 源码(资深)
打开 packages/testing/testing-module.builder.ts,回答:
overrideProvider()返回的OverrideBy对象包含哪些方法?compile()方法在何时应用 override?是在扫描前还是扫描后?TestingModule为什么要继承NestApplicationContext而不是NestApplication?
九、本课知识点总结
| 知识点 | 要点 |
|---|---|
| @nestjs/swagger | 基于装饰器自动生成 OpenAPI 文档 |
| DocumentBuilder | .setTitle() / .setDescription() / .setVersion() / .addBearerAuth() / .build() |
| SwaggerModule | .createDocument(app, config) + .setup('api', app, document) |
| @ApiProperty() | 标注 DTO 属性的类型、描述、示例、约束 |
| @ApiTags() | 控制器分组 |
| 响应装饰器 | 25+ 个(@ApiOkResponse、@ApiCreatedResponse 等) |
| CLI Plugin | nest-cli.json 配置后自动推断 @ApiProperty |
| 映射类型 | 从 @nestjs/swagger 导入以保留 API 元数据 |
| 多 Spec | include 选项为不同模块生成独立文档 |
| Schema 组合 | oneOf / anyOf / allOf + getSchemaPath() |
| @nestjs/testing | Test.createTestingModule().compile() |
| overrideProvider | .useValue() / .useClass() / .useFactory() |
| useMocker | 自动 mock 所有未注册依赖 |
| E2E 测试 | createNestApplication() + supertest |
| overrideGuard | 跳过认证 { canActivate: () => true } |
| overrideModule | .useModule() 替换整个模块 |
| Automock | TestBed.create(Service).compile() 自动类型安全 mock |
| REPL | --entryFile repl,get() / debug() / methods() |
| Request-scoped 测试 | ContextIdFactory.create() + app.resolve() |
| 全局 Guard 测试 | useExisting 使 Guard 可被 override |
| TestingModuleBuilder 源码 | override 链模式,Map 存储替换映射 |
| TestingModule 源码 | 继承 NestApplicationContext,复用生产 DI 容器 |
| 测试金字塔 | 单元 70% → 集成 20% → E2E 10% |
| API 文档即合约 | Swagger 作为前后端协作的唯一真相来源 |
| 覆盖率目标 | 80%+ 代码覆盖率 |
下一课预告:第二十课将进入生产部署与架构设计,学习 Docker 部署、健康检查、性能优化、CQRS 架构、Serverless、DevTools 等内容,并以 NestJS 技术栈全景图收尾整个课程。