公司动态

第13章 nestjs服务端开发:单元测试

📅 2026/9/1 19:41:58
第13章 nestjs服务端开发:单元测试
nestjs测试概述什么是单元测试e2e测试单元测试 Unit Test核心思想隔离原则只测试目标类本身所有外部依赖其他 Service、Repository、Http 调用、数据库全部mock模拟不调用真实实现。目标验证「输入 → 内部逻辑 → 输出」是否正确不依赖数据库、网络、其他模块。运行速度极快。常用 APINest JestTestingModule构造测试模块替代真实AppModuleproviders注册被测类 mock 依赖overrideProvider()覆盖原有 provider注入 mock 对象jest.fn()/jest.mock()创建模拟函数getT()从测试模块拿到类实例典型目录结构src └── user ├── user.service.ts └── user.service.spec.ts # 单元测试文件一一对应完整示例Service 单元测试user.service.tsimport { Injectable } from nestjs/common; import { UserRepository } from ./user.repository; Injectable() export class UserService { constructor(private readonly userRepo: UserRepository) {} async getUserById(id: number) { return this.userRepo.findOneById(id); } }user.service.spec.tsimport { Test, TestingModule } from nestjs/testing; import { UserService } from ./user.service; import { UserRepository } from ./user.repository; // 创建mock仓库 const mockUserRepo { findOneById: jest.fn() }; describe(UserService, () { let service: UserService; // 每个测试用例前构建测试模块 beforeEach(async () { const moduleFixture: TestingModule await Test.createTestingModule({ providers: [ UserService, { provide: UserRepository, useValue: mockUserRepo } ] }).compile(); service moduleFixture.getlt;UserServicegt;(UserService); }); // 每个用例后清空mock调用记录 afterEach(() { jest.clearAllMocks(); }); describe(getUserById, () { it(应该调用仓库findOneById并返回用户数据, async () { // mock返回值 const mockUser {id:1,name:test}; mockUserRepo.findOneById.mockResolvedValue(mockUser); const result await service.getUserById(1); // 断言 expect(mockUserRepo.findOneById).toHaveBeenCalledWith(1); expect(result).toEqual(mockUser); }); }); });单元测试适用场景 优缺点适合Service 业务逻辑Pipe、Guard、Interceptor、Filter 工具类工具函数、DTO 校验逻辑不适合完整接口请求链路数据库真实交互中间件、路由、装饰器运行效果优点运行速度快可精准定位代码哪一行出错CI 频繁跑不需要数据库。缺点需要大量 mock无法验证 Nest 框架装饰器、路由、中间件是否生效无法验证真实数据库行为。单元测试关注点代码逻辑正确性E2E 端到端测试 end‑to‑end核心思想启动完整 Nest 应用实例模拟真实 HTTP 客户端supertest发送真实请求GET /users/1完整走一遍HttpRequest →中间件→Guard→Interceptor→Controller→Service→Repository→数据库→Response不 mock 整个链路可以使用真实数据库测试库验证整套系统协同工作。目录位置认独立文件夹test/app.e2e‑spec.ts和 src 平级。关键 APITest.createNestApplication()构建完整 nest 应用实例和生产启动几乎一样app.getHttpServer()拿到 http 服务实例交给 supertestrequest(app.getHttpServer())发送 http 请求可以重写模块依赖例如把生产数据库替换为测试数据库。最简 E2E 示例import { Test, TestingModule } from nestjs/testing; import { INestApplication } from nestjs/common; import * as request from supertest; import { AppModule } from ../src/app.module; describe(AppController (e2e), () { let app: INestApplication; // 在所有用例之前启动完整应用 beforeAll(async () { const moduleFixture: TestingModule await Test.createTestingModule({ imports: [AppModule], }).compile(); app moduleFixture.createNestApplication(); await app.init(); // 启动应用执行onModuleInit等生命周期钩子 }); // 全部测试结束关闭服务 afterAll(async () { await app.close(); }); it(/ (GET), async () { return request(app.getHttpServer()) .get(/) .expect(200) .expect(Hello World!); }); });E2E 适用场景 优缺点适合HTTP 接口完整链路测试路由、状态码、响应体Guard、Interceptor、Pipe、Filter 真实运行效果DTO 全局校验、异常过滤器返回格式模块之间集成、数据库交互验证接口文档约定是否符合预期不适合细粒度业务逻辑逻辑错应该交给单元测试大量高频执行E2E 速度慢优点最贴近真实用户行为可以发现框架层、配置、模块集成的 bug验证 http 状态码、header、异常返回格式。缺点运行慢依赖数据库环境准备复杂bug 定位没有单元测试精准。单元测试 vs E2E 测试对比表对比项单元测试 UnitE2E 端到端测试被测对象单个类Service/Controller/Pipe/Guard完整 Nest 应用真实 HTTP 请求依赖处理全部外部依赖做 Mock 模拟使用真实依赖数据库、模块少量 mock是否启动 HTTP 服务❌不启动直接调用类方法✅完整启动 Nest Http 服务测试文件位置和业务文件同级xxx.spec.tstest/**/*.e2e‑spec.ts运行速度极快毫秒级慢秒级涉及数据库关注点内部业务逻辑正确性接口契约、模块协同、框架行为适合测业务逻辑、工具类路由、拦截器、守卫、过滤器、数据库交互缺点大量 mock无法验证装饰器运行效果启动开销大依赖外部环境定位 bug 麻烦Nest 测试最佳实践分层写测试Service、工具函数大量写单元测试Controller 不要写大量单元测试交给 E2E 验证接口Controller 单元测试仅做简单逻辑校验Guard / Pipe / Interceptor优先单元测试少量 E2E 覆盖真实 http 场景执行命令区分# 执行单元测试 npm run test 监听模式开发单元测试 npm run test:watch 执行E2E测试 npm run test:e2e 输出测试覆盖率 npm run test:cov测试覆盖率单元测试负责拉高覆盖率E2E 保证链路跑通不要强制 E2E 追求高覆盖率Mock 原则单元测试所有外部依赖必须 mock不要碰真实数据库。E2E尽量少 mock只有第三方外部 API第三方 http 接口才 mock。生命周期钩子beforeEach / afterEach单元测试每个用例重新构建模块清空 mock。beforeAll / afterAllE2E 测试只启动一次应用测试完成关闭。基础案例控制器单元测试上手与理解DI底层原理先写业务代码user.controller.tsimport { Controller, Get, Param } from nestjs/common; import { UserService } from ./user.service; Controller(user) export class UserController { // 构造器注入UserService 由Nest DI容器负责实例化并传入 constructor(private readonly userService: UserService) {} Get(:id) async getUserInfo(Param(id) id: string) { const numId parseInt(id, 10); const user await this.userService.findUserById(numId); return { code: 200, data: user }; } }user.service.ts被控制器依赖import { Injectable } from nestjs/common; Injectable() export class UserService { async findUserById(id: number) { // 模拟数据库查询 return { id, name: 用户 id }; } }Controller 单元测试文件user.controller.spec.tsimport { Test } from nestjs/testing; import { UserController } from ./user.controller; import { UserService } from ./user.service; describe(UserController, () { let userController: UserController; // 模拟 UserService 的mock对象 const mockUserService { findUserById: jest.fn() }; // 每个it用例执行前重新构建测试模块 beforeEach(async () { const testModule await Test.createTestingModule({ controllers: [UserController], providers: [ { provide: UserService, useValue: mockUserService } ] }).compile(); // 从DI容器中取出控制器实例 userController testModule.getlt;UserControllergt;(UserController); }); describe(getUserInfo, () { it(调用getUserInfo返回包装后的用户数据, async () { // 给mock方法预设返回值 mockUserService.findUserById.mockResolvedValue({ id: 100, name: 用户100 }); // 直接调用控制器方法**不走HTTP网络** const result await userController.getUserInfo(100); // 断言返回结果 expect(result).toEqual({ code: 200, data: { id: 100, name: 用户100 } }); // 断言service方法被调用并且参数正确 expect(mockUserService.findUserById).toHaveBeenCalledWith(100); }); }); });执行测试命令npm run test user.controller.spec.ts逐行深度讲解 DI 底层原理DI 容器是什么Nest 底层Nest 内部维护一个IoC 容器控制反转容器IoCInversion of Control控制反转。类不再自己 new 依赖全部交给容器管理实例的创建、存储、依赖查找、依赖注入。// ❌ 不使用DI自己手动new耦合高不好测试 const service new UserService(); const ctrl new UserController(service);Nest DI 做的事情收集Controller()、Injectable()装饰器标记的类解析类的构造函数参数知道UserController需要一个UserService容器内部实例化UserService再把实例传入UserController的构造函数。这一步是 DI 的核心实例的创建权从调用方转移到了容器UserController不再关心UserService是怎么被 new 出来的只需要在构造函数里声明「我需要一个 UserService」容器就会在启动阶段自动完成实例化并注入。这种「声明依赖、由容器装配」的方式让类与类之间解耦也让测试时可以轻松地用 mock 对象替换真实依赖所有实例存放在容器内部需要时通过get(Class)获取。容器默认是单例模式即同一个类在容器中只维护一个实例所有注入方共享该实例避免重复创建带来的资源浪费也保证了状态的一致性。Injectable()不是魔法它只是给类打上标记告诉 Nest DI 容器这个类可以被容器托管可以被注入。TestingModule 就是一个轻量版的 IoC 容器Test.createTestingModule()创建一个独立的测试容器和生产环境 App 的容器互相隔离。controllers: [UserController]告诉测试容器托管这个控制器providers注册提供者{provide: UserService, useValue: mockUserService}providetoken令牌DI 容器依靠 token 去找对应的实例useValue告诉容器当别人请求UserService这个 token 时不要 new 真实 UserService直接返回我们写好的 mock 对象。compile()做了什么const testModule await Test.createTestingModule({...}).compile();compile()触发 DI 容器解析所有依赖关系实例化所有控制器、provider构建完成容器上下文。控制器单元测试不需要createNestApplication()和app.init()那是 E2E 启动 HTTP 服务器才需要的。单元测试只操作内存中的对象实例。testModule.getUserController(UserController)从已经构建好的 DI 容器中根据 token 取出已经实例化完成的控制器对象。 等价于容器内部帮我们完成// 容器内部伪代码 const instance new UserController(mockUserService);测试用例关键点const result await userController.getUserInfo(100);⚠️ 重点单元测试直接调用控制器类的方法没有经过 HTTP没有路由没有装饰器解析逻辑。Get(:id)、Param(id)这些装饰器是 Nest HTTP 层在处理单元测试不会执行这部分逻辑。 所以单元测试只能测控制器内部方法逻辑路由、参数装饰器解析是否正常要交给 E2E 测试验证。mockUserService.findUserById.mockResolvedValue(...)jest mock 函数模拟 service 异步返回完全不会运行真实 UserService 代码。expect(mockUserService.findUserById).toHaveBeenCalledWith(100)不仅断言返回结果还断言控制器是否正确把参数传给 service。这是单元测试非常重要的校验点。容易混淆的知识点Token 的三种写法useValue直接提供一个对象、mock 实例本次案例使用。useClass容器会 new 这个类可以传入替换的真实 / 模拟类。useFactory工厂函数动态生成实例。providers: [ { provide: UserService, useClass: MockUserService }, { provide: UserService, useFactory: () ({findUserById: jest.fn()}) } ]单元测试 vs E2E 在 Controller 上的区别总结点Controller 单元测试E2E 测试获取控制器testModule.get(UserController)不直接拿控制器走 http 请求Get/Param 等装饰器不生效完整生效UserServiceMock 模拟对象使用真实实例HTTP 服务不启动完整启动测试目标控制器内部业务逻辑路由、参数解析、完整链路DI 底层小拓展为什么构造器注入是 Nest 推荐方式依赖一目了然看构造函数就知道当前类依赖哪些东西非常利于单元测试测试时可以轻松传入 mock 对象不需要修改源代码符合 IoC 思想类不负责创建依赖只消费依赖。❌ 不推荐属性注入Inject()写在属性上做主要注入可读性差单元测试手动实例化时不方便传参。常见踩坑忘记 compile ()没有 compileDI 容器没有实例化对象调用 get 会报错。单元测试写了 AppModule导入完整 AppModule 会加载大量无关 provider单元测试失去隔离性变慢。误以为单元测试会执行 Param、Get 逻辑不会这部分属于 HTTP 层需要 E2E 验证。mock 写错 tokenprovide 的 token 必须和构造函数依赖的类完全一致否则 DI 拿不到 mock会创建真实对象。单元测试登录流程AuthService测试用例