一、为什么要写单元测试
- 保持业务逻辑稳定,防回归。
- 提高重构信心。
- 文档化接口/行为(测试即规格)。
- 更快定位 bug(单元测试通常比手动或集成测试更细粒度、更快)。
二、Nest 单元测试的基本思路
- 只测试单元(一个类 / 函数):不要启动真实服务器、不要访问真实数据库或第三方服务(因此需要使用 mock 数据),我们只是测试代码逻辑,不测试数据库操作 / 传参(class-transformer...) 等等。
- 使用依赖注入(DI)+ mock :通过 Nest 的
Test.createTestingModule
创建模块,把依赖替换成 mock(手工或 jest mock)。 - 断言行为而非实现:关注方法返回、调用了哪些依赖、抛出哪些错误等。
- 异步处理要正确 await :测试 async 函数或返回 Promise 的方法时必须
await
或使用 Jest 的resolves/rejects
。
三、常用工具与配置(快速)
- 测试框架:Jest (
@nestjs/testing
与ts-jest
) - Mock:Jest 的
jest.fn()
、jest.spyOn()
或手动提供假的对象 - 运行脚本(
package.json
):
json
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage"
}
四、示例代码
假设有一个简单业务:UsersService
提供 findById
,内部依赖 UserRepository
(模拟 TypeORM repository)。
目录:
src/
users/
users.service.ts
users.controller.ts
dto/
guards/
test/
users.service.spec.ts
users.controller.spec.ts
users.service.ts
ts
// src/users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
@Injectable()
export class UsersService {
constructor(private readonly userRepo: any /* 实际为 Repository<User> */) {}
async findById(id: number) {
const user = await this.userRepo.findOne({ where: { id } });
if (!user) throw new NotFoundException(`User ${id} not found`);
return user;
}
async create(dto: any) {
return this.userRepo.save(dto);
}
}
users.service.spec.ts
(单元测试:mock repository)
ts
// test/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from '../src/users/users.service';
import { NotFoundException } from '@nestjs/common';
describe('UsersService', () => {
let service: UsersService;
// 手工创建 repo mock
const mockRepo = {
findOne: jest.fn(),
save: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{ provide: 'UserRepository', useValue: mockRepo }, // 以 token 注入时用这种方式
],
})
// 如果是 constructor 注入具体 token,需要手动替换 providers 的注入,下面示例假设 UsersService 接收 repo 作为第一参数
.compile();
// 直接 new 或者 get 如果服务构造签名匹配
service = new UsersService(mockRepo);
jest.clearAllMocks();
});
it('findById - 返回用户', async () => {
const fake = { id: 1, name: 'Alice' };
mockRepo.findOne.mockResolvedValue(fake);
const res = await service.findById(1);
expect(res).toEqual(fake);
expect(mockRepo.findOne).toHaveBeenCalledWith({ where: { id: 1 } });
});
it('findById - 找不到抛 404', async () => {
mockRepo.findOne.mockResolvedValue(null);
await expect(service.findById(999)).rejects.toThrow(NotFoundException);
});
it('create - 调用 save 并返回', async () => {
const dto = { name: 'Bob' };
mockRepo.save.mockResolvedValue({ id: 2, ...dto });
const res = await service.create(dto);
expect(res).toEqual({ id: 2, name: 'Bob' });
expect(mockRepo.save).toHaveBeenCalledWith(dto);
});
});
要点说明:
- 没有启动 Nest 应用或数据库。
mockRepo
使用jest.fn()
可精确断言调用参数与次数。- 测试关注返回值与错误抛出。
users.controller.ts
ts
// src/users/users.controller.ts
import { Controller, Get, Param } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':id')
async getUser(@Param('id') id: string) {
return this.usersService.findById(Number(id));
}
}
users.controller.spec.ts
(Controller 的单元测试:mock Service)
ts
// test/users.controller.spec.ts
import { UsersController } from '../src/users/users.controller';
import { UsersService } from '../src/users/users.service';
describe('UsersController', () => {
let controller: UsersController;
const mockService = { findById: jest.fn() };
beforeEach(() => {
controller = new UsersController(mockService as any);
jest.clearAllMocks();
});
it('getUser - 返回调用结果', async () => {
mockService.findById.mockResolvedValue({ id: 1, name: 'Alice' });
const res = await controller.getUser('1');
expect(res).toEqual({ id: 1, name: 'Alice' });
expect(mockService.findById).toHaveBeenCalledWith(1);
});
});
要点:
- Controller 测试仅关心路由层调用 service 的行为,不测试 service 的实现(分离关注点)。
测试 Guard / Pipe 的示例
Guard、Pipe、Interceptor 都是可以单独实例化并直接调用对应方法测试的。例如测试一个简单 AuthGuard
:
ts
// test/auth.guard.spec.ts
import { ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '../src/auth/auth.guard';
describe('AuthGuard', () => {
let guard: AuthGuard;
beforeEach(() => guard = new AuthGuard());
it('canActivate - 允许已登录用户', async () => {
const ctx = {
switchToHttp: () => ({
getRequest: () => ({ user: { id: 1 } }),
}),
} as unknown as ExecutionContext;
await expect(guard.canActivate(ctx)).resolves.toBe(true);
});
it('canActivate - 拒绝无 user 的请求', async () => {
const ctx = {
switchToHttp: () => ({ getRequest: () => ({}) }),
} as unknown as ExecutionContext;
await expect(guard.canActivate(ctx)).resolves.toBe(false);
});
});