Appearance
NestJS 测试 (Testing)
NestJS 提供了强大的测试工具和框架集成,支持单元测试、集成测试和端到端测试。NestJS 的测试功能基于 Jest(默认)和 Supertest,同时也支持其他测试框架如 Mocha。
基础概念
NestJS 测试包含:
- 单元测试 - 测试单个类、方法或函数
- 集成测试 - 测试多个组件之间的交互
- 端到端测试 - 测试整个应用程序流程
- 测试工具 - 提供测试实用工具和模拟功能
测试环境设置
安装测试依赖
bash
npm install --save-dev jest @types/jest supertest @types/supertest
Jest 配置
json
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s",
"!**/node_modules/**",
"!**/coverage/**",
"!**/dist/**"
]
}
单元测试
服务单元测试
typescript
// cats.service.ts
import { Injectable } from '@nestjs/common';
export interface Cat {
id: number;
name: string;
breed: string;
}
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
create(cat: Cat): Cat {
this.cats.push(cat);
return cat;
}
findAll(): Cat[] {
return this.cats;
}
findOne(id: number): Cat {
return this.cats.find(cat => cat.id === id);
}
}
typescript
// cats.service.spec.ts
import { Test } from '@nestjs/testing';
import { CatsService } from './cats.service';
describe('CatsService', () => {
let service: CatsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [CatsService],
}).compile();
service = moduleRef.get<CatsService>(CatsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('create', () => {
it('should add a cat to the array', () => {
const cat = { id: 1, name: 'Fluffy', breed: 'Persian' };
const result = service.create(cat);
expect(result).toEqual(cat);
expect(service.findAll()).toContain(cat);
});
});
describe('findAll', () => {
it('should return an array of cats', () => {
const cat = { id: 1, name: 'Fluffy', breed: 'Persian' };
service.create(cat);
expect(service.findAll()).toEqual([cat]);
});
});
describe('findOne', () => {
it('should return a specific cat', () => {
const cat = { id: 1, name: 'Fluffy', breed: 'Persian' };
service.create(cat);
expect(service.findOne(1)).toEqual(cat);
});
it('should return undefined for non-existent cat', () => {
expect(service.findOne(999)).toBeUndefined();
});
});
});
控制器单元测试
typescript
// cats.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { CatsService, Cat } from './cats.service';
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Post()
create(@Body() createCatDto: Cat) {
return this.catsService.create(createCatDto);
}
@Get()
findAll(): Cat[] {
return this.catsService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.catsService.findOne(+id);
}
}
typescript
// cats.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
describe('CatsController', () => {
let controller: CatsController;
let service: CatsService;
beforeEach(async () => {
const moduleRef: TestingModule = await Test.createTestingModule({
controllers: [CatsController],
providers: [
{
provide: CatsService,
useValue: {
create: jest.fn(),
findAll: jest.fn(),
findOne: jest.fn(),
},
},
],
}).compile();
controller = moduleRef.get<CatsController>(CatsController);
service = moduleRef.get<CatsService>(CatsService);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('create', () => {
it('should call catsService.create', () => {
const cat = { id: 1, name: 'Fluffy', breed: 'Persian' };
jest.spyOn(service, 'create').mockReturnValue(cat);
expect(controller.create(cat)).toEqual(cat);
expect(service.create).toHaveBeenCalledWith(cat);
});
});
describe('findAll', () => {
it('should call catsService.findAll', () => {
const cats = [{ id: 1, name: 'Fluffy', breed: 'Persian' }];
jest.spyOn(service, 'findAll').mockReturnValue(cats);
expect(controller.findAll()).toEqual(cats);
expect(service.findAll).toHaveBeenCalled();
});
});
describe('findOne', () => {
it('should call catsService.findOne with correct id', () => {
const cat = { id: 1, name: 'Fluffy', breed: 'Persian' };
jest.spyOn(service, 'findOne').mockReturnValue(cat);
expect(controller.findOne('1')).toEqual(cat);
expect(service.findOne).toHaveBeenCalledWith(1);
});
});
});
集成测试
模块集成测试
typescript
// cats.module.int-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { CatsModule } from './cats.module';
import { CatsService } from './cats.service';
import { CatsController } from './cats.controller';
describe('CatsModule Integration', () => {
let service: CatsService;
let controller: CatsController;
beforeEach(async () => {
const moduleRef: TestingModule = await Test.createTestingModule({
imports: [CatsModule],
}).compile();
service = moduleRef.get<CatsService>(CatsService);
controller = moduleRef.get<CatsController>(CatsController);
});
it('should resolve CatsService and CatsController', () => {
expect(service).toBeDefined();
expect(controller).toBeDefined();
});
it('should create and retrieve a cat', () => {
const cat = { id: 1, name: 'Fluffy', breed: 'Persian' };
const createdCat = service.create(cat);
expect(createdCat).toEqual(cat);
const retrievedCat = service.findOne(1);
expect(retrievedCat).toEqual(cat);
});
});
依赖注入测试
typescript
// database.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class DatabaseService {
async connect() {
// 连接数据库
return true;
}
async disconnect() {
// 断开数据库连接
return true;
}
}
// user.service.ts
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class UserService {
constructor(@Inject('DATABASE_SERVICE') private dbService: any) {}
async createUser(userData: any) {
await this.dbService.connect();
// 创建用户逻辑
return { id: 1, ...userData };
}
}
typescript
// user.service.spec.ts
import { Test } from '@nestjs/testing';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
let mockDbService: any;
beforeEach(async () => {
mockDbService = {
connect: jest.fn().mockResolvedValue(true),
disconnect: jest.fn().mockResolvedValue(true),
};
const moduleRef = await Test.createTestingModule({
providers: [
UserService,
{
provide: 'DATABASE_SERVICE',
useValue: mockDbService,
},
],
}).compile();
service = moduleRef.get<UserService>(UserService);
});
it('should create a user', async () => {
const userData = { name: 'John', email: 'john@example.com' };
const result = await service.createUser(userData);
expect(mockDbService.connect).toHaveBeenCalled();
expect(result).toEqual({ id: 1, ...userData });
});
});
端到端测试
应用程序端到端测试
typescript
// app.e2e-spec.ts
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;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
afterAll(async () => {
await app.close();
});
});
控制器端到端测试
typescript
// cats.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { CatsModule } from '../src/cats/cats.module';
describe('CatsController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [CatsModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/cats (GET)', () => {
return request(app.getHttpServer())
.get('/cats')
.expect(200)
.expect([]);
});
it('/cats (POST)', () => {
return request(app.getHttpServer())
.post('/cats')
.send({ name: 'Fluffy', breed: 'Persian' })
.expect(201)
.expect({
id: expect.any(Number),
name: 'Fluffy',
breed: 'Persian',
});
});
it('/cats/:id (GET)', () => {
return request(app.getHttpServer())
.post('/cats')
.send({ name: 'Fluffy', breed: 'Persian' })
.then(response => {
const id = response.body.id;
return request(app.getHttpServer())
.get(`/cats/${id}`)
.expect(200)
.expect({
id: id,
name: 'Fluffy',
breed: 'Persian',
});
});
});
afterAll(async () => {
await app.close();
});
});
Mocking 和 Spying
Mocking 服务
typescript
// mocking example
describe('CatsService with mocked dependencies', () => {
let service: CatsService;
let loggerService: any;
beforeEach(async () => {
loggerService = {
log: jest.fn(),
error: jest.fn(),
};
const moduleRef = await Test.createTestingModule({
providers: [
CatsService,
{
provide: 'LoggerService',
useValue: loggerService,
},
],
}).compile();
service = moduleRef.get<CatsService>(CatsService);
});
it('should log when creating a cat', () => {
const cat = { id: 1, name: 'Fluffy', breed: 'Persian' };
service.create(cat);
expect(loggerService.log).toHaveBeenCalledWith('Cat created', cat);
});
});
使用 Jest Spies
typescript
describe('Service with spies', () => {
let service: CatsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [CatsService],
}).compile();
service = moduleRef.get<CatsService>(CatsService);
});
it('should call internal method when creating cat', () => {
const internalMethodSpy = jest.spyOn(service as any, 'internalValidation');
const cat = { id: 1, name: 'Fluffy', breed: 'Persian' };
service.create(cat);
expect(internalMethodSpy).toHaveBeenCalledWith(cat);
});
});
测试工具
Test.createTestingModule
typescript
// 使用 Test.createTestingModule 创建测试模块
import { Test } from '@nestjs/testing';
describe('Complex service test', () => {
it('should test service with multiple dependencies', async () => {
const moduleRef = await Test.createTestingModule({
providers: [
CatsService,
{
provide: 'DatabaseConnection',
useValue: mockDatabaseConnection,
},
{
provide: 'ConfigService',
useValue: mockConfigService,
},
],
}).compile();
const service = moduleRef.get<CatsService>(CatsService);
// 执行测试
expect(service).toBeDefined();
});
});
overrideProvider
typescript
describe('Service with overridden provider', () => {
let service: CatsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [CatsService],
})
.overrideProvider('DatabaseConnection')
.useValue(mockDatabaseConnectionWithFailures)
.compile();
service = moduleRef.get<CatsService>(CatsService);
});
it('should handle database failures gracefully', async () => {
// 测试错误处理逻辑
await expect(service.findAll()).rejects.toThrow();
});
});
异步测试
异步服务测试
typescript
// async service
@Injectable()
export class AsyncCatsService {
async createAsync(cat: Cat): Promise<Cat> {
// 模拟异步操作
await new Promise(resolve => setTimeout(resolve, 100));
return { ...cat, id: Date.now() };
}
async findAllAsync(): Promise<Cat[]> {
await new Promise(resolve => setTimeout(resolve, 50));
return [];
}
}
// async test
describe('AsyncCatsService', () => {
let service: AsyncCatsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [AsyncCatsService],
}).compile();
service = moduleRef.get<AsyncCatsService>(AsyncCatsService);
});
it('should create cat asynchronously', async () => {
const cat = { name: 'Fluffy', breed: 'Persian' };
const result = await service.createAsync(cat);
expect(result).toBeDefined();
expect(result.name).toBe('Fluffy');
expect(result.breed).toBe('Persian');
expect(result.id).toBeDefined();
});
it('should find all cats asynchronously', async () => {
const result = await service.findAllAsync();
expect(result).toEqual([]);
});
});
使用 done 回调
typescript
describe('Service with done callback', () => {
let service: AsyncCatsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [AsyncCatsService],
}).compile();
service = moduleRef.get<AsyncCatsService>(AsyncCatsService);
});
it('should handle async operation with done callback', (done) => {
service.createAsync({ name: 'Fluffy', breed: 'Persian' })
.then(result => {
expect(result).toBeDefined();
done();
})
.catch(done);
});
});
测试数据库集成
内存数据库测试
typescript
// 使用 SQLite 内存数据库进行测试
import { Test } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Cat } from '../src/cat.entity';
import { CatsService } from '../src/cats.service';
describe('CatsService with database', () => {
let service: CatsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
entities: [Cat],
synchronize: true,
}),
TypeOrmModule.forFeature([Cat]),
],
providers: [CatsService],
}).compile();
service = moduleRef.get<CatsService>(CatsService);
});
it('should save cat to database', async () => {
const cat = { name: 'Fluffy', breed: 'Persian' };
const savedCat = await service.create(cat);
expect(savedCat.id).toBeDefined();
expect(savedCat.name).toBe('Fluffy');
});
});
测试微服务
微服务测试
typescript
// math.controller.ts (微服务)
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
@Controller()
export class MathController {
@MessagePattern({ cmd: 'sum' })
sum(data: number[]): number {
return (data || []).reduce((a, b) => a + b, 0);
}
}
// math.controller.spec.ts
import { Test } from '@nestjs/testing';
import { MathController } from './math.controller';
describe('MathController', () => {
let controller: MathController;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [MathController],
}).compile();
controller = moduleRef.get<MathController>(MathController);
});
it('should sum an array of numbers', () => {
const result = controller.sum([1, 2, 3, 4, 5]);
expect(result).toBe(15);
});
it('should return 0 for empty array', () => {
const result = controller.sum([]);
expect(result).toBe(0);
});
});
测试管道、守卫、拦截器和过滤器
管道测试
typescript
// validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
if (!value) {
throw new BadRequestException('Value is required');
}
return value;
}
}
// validation.pipe.spec.ts
import { ValidationPipe } from './validation.pipe';
describe('ValidationPipe', () => {
let pipe: ValidationPipe;
beforeEach(() => {
pipe = new ValidationPipe();
});
it('should return the value if it exists', () => {
const value = { name: 'Fluffy' };
expect(pipe.transform(value, {} as ArgumentMetadata)).toEqual(value);
});
it('should throw an error if value is empty', () => {
expect(() => {
pipe.transform(null, {} as ArgumentMetadata);
}).toThrow(BadRequestException);
});
});
守卫测试
typescript
// auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
return request.headers.authorization === 'Bearer valid-token';
}
}
// auth.guard.spec.ts
import { AuthGuard } from './auth.guard';
describe('AuthGuard', () => {
let guard: AuthGuard;
beforeEach(() => {
guard = new AuthGuard();
});
it('should allow access with valid token', () => {
const context = {
switchToHttp: jest.fn(() => ({
getRequest: jest.fn(() => ({
headers: { authorization: 'Bearer valid-token' },
})),
})),
} as ExecutionContext;
expect(guard.canActivate(context)).toBe(true);
});
it('should deny access with invalid token', () => {
const context = {
switchToHttp: jest.fn(() => ({
getRequest: jest.fn(() => ({
headers: { authorization: 'Bearer invalid-token' },
})),
})),
} as ExecutionContext;
expect(guard.canActivate(context)).toBe(false);
});
});
测试最佳实践
1. 测试结构
typescript
// 推荐的测试结构
describe('Feature Name', () => {
// 设置测试环境
let service: MyService;
beforeEach(async () => {
// 准备测试数据
const moduleRef = await Test.createTestingModule({
providers: [MyService],
}).compile();
service = moduleRef.get<MyService>(MyService);
});
// 测试不同的场景
describe('specific method', () => {
it('should handle normal case', () => {
// 测试正常情况
});
it('should handle edge case', () => {
// 测试边界情况
});
it('should handle error case', () => {
// 测试错误情况
});
});
});
2. 测试数据工厂
typescript
// test-data.factory.ts
export class TestDataFactory {
static createCat(overrides: Partial<Cat> = {}): Cat {
return {
id: 1,
name: 'Test Cat',
breed: 'Test Breed',
...overrides,
};
}
static createCats(count: number): Cat[] {
return Array.from({ length: count }, (_, i) =>
this.createCat({ id: i + 1, name: `Cat ${i + 1}` })
);
}
}
// 在测试中使用
import { TestDataFactory } from './test-data.factory';
describe('CatsService', () => {
it('should create a cat with custom data', () => {
const customCat = TestDataFactory.createCat({ name: 'Custom Name' });
const result = service.create(customCat);
expect(result.name).toBe('Custom Name');
});
});
3. 测试覆盖率
json
// package.json 中的测试脚本
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand --no-cache"
}
}
4. 测试环境变量
typescript
// test.config.ts
export const testConfig = {
database: {
host: process.env.TEST_DB_HOST || 'localhost',
port: parseInt(process.env.TEST_DB_PORT, 10) || 5432,
},
api: {
baseUrl: process.env.TEST_API_BASE_URL || 'http://localhost:3000',
},
};
性能测试
基准测试
typescript
describe('Performance Tests', () => {
let service: CatsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [CatsService],
}).compile();
service = moduleRef.get<CatsService>(CatsService);
});
it('should handle large datasets efficiently', async () => {
const startTime = Date.now();
// 创建大量数据进行测试
for (let i = 0; i < 1000; i++) {
service.create({ id: i, name: `Cat ${i}`, breed: 'Breed' });
}
const endTime = Date.now();
const duration = endTime - startTime;
// 确保操作在合理时间内完成
expect(duration).toBeLessThan(1000); // 小于1秒
});
});
测试运行器配置
Jest 配置高级选项
javascript
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
collectCoverageFrom: [
'src/**/*.ts',
'!src/main.ts',
'!src/**/index.ts',
],
coverageDirectory: '../coverage',
coverageReporters: ['text', 'lcov', 'html'],
testTimeout: 10000, // 10秒超时
setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
};
CI/CD 集成
GitHub Actions 测试配置
yaml
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run test:cov
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
总结
NestJS 提供了全面的测试工具和框架,支持各种类型的测试。通过适当的测试策略和最佳实践,可以确保应用程序的质量和可靠性。
测试的主要特点:
- 支持单元、集成和端到端测试
- 提供强大的测试工具和模拟功能
- 与主流测试框架集成
- 支持异步测试
- 提供测试覆盖率工具
- 支持各种测试场景(管道、守卫、拦截器等)
- 包含性能测试方法
- 支持 CI/CD 集成