第十九课:API 文档与测试 — 质量保障

4 阅读15分钟

覆盖文档: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 Schema
  • http://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() 常用选项:

选项类型说明
descriptionstring属性描述
exampleany示例值
requiredboolean是否必填(默认 true
typeType类型(复杂类型时必须指定)
enumany[]枚举值列表
defaultany默认值
minimum / maximumnumber数值范围
minLength / maxLengthnumber字符串长度范围
isArrayboolean是否为数组
nullableboolean是否可为 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 组合

使用 oneOfanyOfallOf 描述复杂类型关系:

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)            │
└─────────────────────────────────────────────────────┘

核心原则:

  1. 代码即文档:通过装饰器在代码中定义文档,保证文档与实现同步
  2. 自动化验证:在 CI 中验证 API 是否符合 OpenAPI Schema
  3. 客户端代码生成:从 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 + useMockerAutomock(@automock/jest)
E2E 测试supertestPactum
覆盖率80%+ 全局阈值按模块设置不同阈值
契约测试OpenAPI Schema diffPact
文档生成Swagger UI + CompodocSwagger 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,回答:

  1. overrideProvider() 返回的 OverrideBy 对象包含哪些方法?
  2. compile() 方法在何时应用 override?是在扫描前还是扫描后?
  3. 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 Pluginnest-cli.json 配置后自动推断 @ApiProperty
映射类型@nestjs/swagger 导入以保留 API 元数据
多 Specinclude 选项为不同模块生成独立文档
Schema 组合oneOf / anyOf / allOf + getSchemaPath()
@nestjs/testingTest.createTestingModule().compile()
overrideProvider.useValue() / .useClass() / .useFactory()
useMocker自动 mock 所有未注册依赖
E2E 测试createNestApplication() + supertest
overrideGuard跳过认证 { canActivate: () => true }
overrideModule.useModule() 替换整个模块
AutomockTestBed.create(Service).compile() 自动类型安全 mock
REPL--entryFile replget() / 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 技术栈全景图收尾整个课程。