NestJS单元测试 #
测试概述 #
NestJS提供了完善的测试支持,默认使用Jest作为测试框架。单元测试用于测试独立的代码单元,如服务、控制器等。
Jest配置 #
jest.config.js #
javascript
module.exports = {
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: 'src',
testRegex: '.*\\.spec\\.ts$',
transform: {
'^.+\\.(t|j)s$': 'ts-jest',
},
collectCoverageFrom: [
'**/*.(t|j)s',
],
coverageDirectory: '../coverage',
testEnvironment: 'node',
};
测试服务 #
基本服务测试 #
typescript
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
模拟依赖 #
typescript
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { UsersRepository } from './users.repository';
describe('UsersService', () => {
let service: UsersService;
let repository: jest.Mocked<UsersRepository>;
const mockRepository = {
find: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
delete: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: UsersRepository,
useValue: mockRepository,
},
],
}).compile();
service = module.get<UsersService>(UsersService);
repository = module.get(UsersRepository);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('findAll', () => {
it('should return an array of users', async () => {
const mockUsers = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
];
mockRepository.find.mockResolvedValue(mockUsers);
const result = await service.findAll();
expect(result).toEqual(mockUsers);
expect(mockRepository.find).toHaveBeenCalled();
});
});
describe('findOne', () => {
it('should return a user by id', async () => {
const mockUser = { id: 1, name: 'John' };
mockRepository.findOne.mockResolvedValue(mockUser);
const result = await service.findOne(1);
expect(result).toEqual(mockUser);
expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { id: 1 } });
});
it('should return null if user not found', async () => {
mockRepository.findOne.mockResolvedValue(null);
const result = await service.findOne(999);
expect(result).toBeNull();
});
});
});
测试异常 #
typescript
describe('create', () => {
it('should throw ConflictException if email exists', async () => {
const createUserDto = {
name: 'John',
email: 'john@example.com',
password: 'password',
};
mockRepository.findOne.mockResolvedValue({ id: 1, email: createUserDto.email });
await expect(service.create(createUserDto)).rejects.toThrow(ConflictException);
});
it('should create a new user', async () => {
const createUserDto = {
name: 'John',
email: 'john@example.com',
password: 'password',
};
mockRepository.findOne.mockResolvedValue(null);
mockRepository.create.mockReturnValue({ id: 1, ...createUserDto });
mockRepository.save.mockResolvedValue({ id: 1, ...createUserDto });
const result = await service.create(createUserDto);
expect(result).toEqual({ id: 1, ...createUserDto });
});
});
测试控制器 #
基本控制器测试 #
typescript
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
describe('UsersController', () => {
let controller: UsersController;
let service: jest.Mocked<UsersService>;
const mockService = {
findAll: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{
provide: UsersService,
useValue: mockService,
},
],
}).compile();
controller = module.get<UsersController>(UsersController);
service = module.get(UsersService);
});
describe('findAll', () => {
it('should return an array of users', async () => {
const mockUsers = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
];
service.findAll.mockResolvedValue(mockUsers);
const result = await controller.findAll();
expect(result).toEqual(mockUsers);
});
});
describe('findOne', () => {
it('should return a user by id', async () => {
const mockUser = { id: 1, name: 'John' };
service.findOne.mockResolvedValue(mockUser);
const result = await controller.findOne(1);
expect(result).toEqual(mockUser);
expect(service.findOne).toHaveBeenCalledWith(1);
});
});
});
测试守卫和装饰器 #
typescript
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
describe('UsersController', () => {
let controller: UsersController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{
provide: UsersService,
useValue: {},
},
],
})
.overrideGuard(JwtAuthGuard)
.useValue({ canActivate: () => true })
.compile();
controller = module.get<UsersController>(UsersController);
});
it('should bypass JwtAuthGuard', () => {
expect(controller).toBeDefined();
});
});
测试管道 #
typescript
import { ValidationPipe } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { CreateUserDto } from './dto/create-user.dto';
describe('CreateUserDto Validation', () => {
let pipe: ValidationPipe;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [ValidationPipe],
}).compile();
pipe = module.get<ValidationPipe>(ValidationPipe);
});
it('should pass validation with valid data', async () => {
const dto = {
name: 'John',
email: 'john@example.com',
password: 'password123',
};
const result = await pipe.transform(dto, {
type: 'body',
metatype: CreateUserDto,
});
expect(result).toEqual(dto);
});
it('should fail validation with invalid email', async () => {
const dto = {
name: 'John',
email: 'invalid-email',
password: 'password123',
};
await expect(
pipe.transform(dto, { type: 'body', metatype: CreateUserDto }),
).rejects.toThrow();
});
});
测试拦截器 #
typescript
import { Test, TestingModule } from '@nestjs/testing';
import { TransformInterceptor } from './transform.interceptor';
import { ExecutionContext, CallHandler } from '@nestjs/common';
import { of } from 'rxjs';
describe('TransformInterceptor', () => {
let interceptor: TransformInterceptor<any>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [TransformInterceptor],
}).compile();
interceptor = module.get<TransformInterceptor<any>>(TransformInterceptor);
});
it('should transform response', (done) => {
const context = {
switchToHttp: () => ({
getRequest: () => ({}),
getResponse: () => ({}),
}),
} as ExecutionContext;
const next: CallHandler = {
handle: () => of({ name: 'John' }),
};
interceptor.intercept(context, next).subscribe({
next: (result) => {
expect(result).toEqual({
code: 200,
message: 'Success',
data: { name: 'John' },
timestamp: expect.any(String),
});
done();
},
});
});
});
测试守卫 #
typescript
import { Test, TestingModule } from '@nestjs/testing';
import { RolesGuard } from './roles.guard';
import { ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
describe('RolesGuard', () => {
let guard: RolesGuard;
let reflector: Reflector;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [RolesGuard, Reflector],
}).compile();
guard = module.get<RolesGuard>(RolesGuard);
reflector = module.get<Reflector>(Reflector);
});
it('should allow access when no roles required', () => {
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(undefined);
const context = {
switchToHttp: () => ({
getRequest: () => ({ user: { role: 'user' } }),
}),
getHandler: () => jest.fn(),
getClass: () => jest.fn(),
} as unknown as ExecutionContext;
expect(guard.canActivate(context)).toBe(true);
});
it('should allow access when user has required role', () => {
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']);
const context = {
switchToHttp: () => ({
getRequest: () => ({ user: { role: 'admin' } }),
}),
getHandler: () => jest.fn(),
getClass: () => jest.fn(),
} as unknown as ExecutionContext;
expect(guard.canActivate(context)).toBe(true);
});
it('should deny access when user lacks required role', () => {
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']);
const context = {
switchToHttp: () => ({
getRequest: () => ({ user: { role: 'user' } }),
}),
getHandler: () => jest.fn(),
getClass: () => jest.fn(),
} as unknown as ExecutionContext;
expect(() => guard.canActivate(context)).toThrow(ForbiddenException);
});
});
测试数据库操作 #
使用内存数据库 #
typescript
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';
describe('UsersService (Integration)', () => {
let service: UsersService;
let repository: Repository<User>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
entities: [User],
synchronize: true,
}),
TypeOrmModule.forFeature([User]),
],
providers: [UsersService],
}).compile();
service = module.get<UsersService>(UsersService);
repository = module.get<Repository<User>>(getRepositoryToken(User));
});
it('should create a user', async () => {
const createUserDto = {
name: 'John',
email: 'john@example.com',
password: 'password',
};
const result = await service.create(createUserDto);
expect(result).toHaveProperty('id');
expect(result.name).toBe(createUserDto.name);
expect(result.email).toBe(createUserDto.email);
});
});
测试覆盖率 #
运行覆盖率测试 #
bash
npm run test:cov
覆盖率报告 #
text
------------------------|----------|----------|----------|----------|
File | % Stmts | % Branch | % Funcs | % Lines |
------------------------|----------|----------|----------|----------|
All files | 90.5 | 85.2 | 92.3 | 90.1 |
users | 95.2 | 90.0 | 100.0 | 95.0 |
users.controller.ts | 100.0 | 100.0 | 100.0 | 100.0 |
users.service.ts | 92.3 | 85.7 | 100.0 | 92.0 |
auth | 85.0 | 80.0 | 85.7 | 84.5 |
auth.service.ts | 85.0 | 80.0 | 85.7 | 84.5 |
------------------------|----------|----------|----------|----------|
最佳实践 #
1. 使用describe分组 #
typescript
describe('UsersService', () => {
describe('findAll', () => {
it('should return users', () => {});
});
describe('findOne', () => {
it('should return user', () => {});
it('should return null if not found', () => {});
});
});
2. 清理模拟 #
typescript
afterEach(() => {
jest.clearAllMocks();
});
3. 使用类型安全的模拟 #
typescript
const mockService = {
findAll: jest.fn() as jest.MockedFunction<UsersService['findAll']>,
};
总结 #
本章学习了NestJS单元测试:
- Jest配置
- 服务测试
- 控制器测试
- 管道、守卫、拦截器测试
- 数据库测试
- 测试覆盖率
接下来,让我们学习 E2E测试。
最后更新:2026-03-28