NestJS 怎么写测试?单元测试、E2E 测试和 Mock 实战
NestJS 内置 Jest 支持,开箱即用。测试分两层:单元测试(测 Service/Controller 的逻辑)和 E2E 测试(测整个请求链路)。关键是学会 Mock 依赖——单元测试不应该依赖数据库或外部服务。
单元测试:测 Service
typescript// users/users.service.spec.ts describe('UsersService', () => { let service: UsersService; let repo: Repository<User>; beforeEach(async () => { const module = await Test.createTestingModule({ providers: [ UsersService, { provide: getRepositoryToken(User), useValue: { findOne: jest.fn(), create: jest.fn(), save: jest.fn(), }, }, ], }).compile(); service = module.get(UsersService); repo = module.get(getRepositoryToken(User)); }); it('findByEmail 应该返回用户', async () => { const mockUser = { id: '1', email: 'test@test.com' }; jest.spyOn(repo, 'findOne').mockResolvedValue(mockUser as User); const result = await service.findByEmail('test@test.com'); expect(result).toEqual(mockUser); expect(repo.findOne).toHaveBeenCalledWith({ where: { email: 'test@test.com' } }); }); });
Test.createTestingModule 创建一个精简的 Nest 容器,只注册需要测试的 Provider。useValue 用 Jest mock 替换真实的 Repository——测试不碰数据库,跑得快而且稳定。
测 Controller
Controller 的测试重点是"路由是否正确调用 Service":
typescriptdescribe('UsersController', () => { let controller: UsersController; let service: UsersService; beforeEach(async () => { const module = await Test.createTestingModule({ controllers: [UsersController], providers: [ { provide: UsersService, useValue: { findOne: jest.fn() } }, ], }).compile(); controller = module.get(UsersController); service = module.get(UsersService); }); it('getUser 应该调用 service.findOne', async () => { jest.spyOn(service, 'findOne').mockResolvedValue({ id: '1', name: 'Test' } as User); await controller.getUser('1'); expect(service.findOne).toHaveBeenCalledWith('1'); }); });
Controller 测试不需要 HTTP 服务器——Nest 的测试工具直接调用方法,省去了网络层开销。
E2E 测试:测完整请求
E2E 测试启动完整的 HTTP 服务器,发真实请求,验证整个链路:
typescript// test/app.e2e-spec.ts describe('App (e2e)', () => { let app: INestApplication; beforeAll(async () => { const moduleFixture = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); await app.init(); }); it('GET /users/:id', () => { return request(app.getHttpServer()) .get('/users/1') .expect(200) .expect({ id: '1', name: 'Test' }); }); afterAll(async () => { await app.close(); }); });
E2E 测试需要数据库。用 testcontainers 启动 Docker 容器里的 Postgres,测完自动销毁:
typescriptimport { PostgreSqlContainer } from '@testcontainers/postgresql'; const container = await new PostgreSqlContainer().start(); process.env.DB_HOST = container.getHost(); process.env.DB_PORT = container.getPort().toString();
Mock 外部服务
如果 Service 调用第三方 API(如支付、邮件),测试时不应该真的调:
typescriptproviders: [ PaymentService, { provide: 'PAYMENT_CLIENT', useValue: { charge: jest.fn().mockResolvedValue({ success: true }) }, }, ],
用自定义 Provider 替换外部服务客户端。测试只验证你的代码逻辑是否正确,不验证第三方服务是否正常。
测试覆盖率
bashnpm run test:cov
目标:Service 层覆盖率 > 80%,Controller 层 > 70%。不要追求 100%——getter/setter、简单委托方法不值得写测试。重点测试业务逻辑分支和边界条件。
测试最佳实践
1. 测试文件和源文件同目录:users.service.spec.ts 放在 users.service.ts 旁边,比放在单独的 __tests__/ 目录更好找。
2. beforeAll vs beforeEach:beforeAll 创建的容器所有测试共享(快),beforeEach 每个测试前重新创建(隔离)。单元测试用 beforeEach 保证隔离,E2E 测试用 beforeAll 加速。
3. 别测试框架本身:不需要测 @Get() 装饰器能不能工作,NestJS 自己有测试。测你的业务逻辑——Service 返回的数据对不对,Controller 有没有调对 Service 方法。