测试金字塔:E2E 测试如何启动完整应用?
在软件开发中,测试是确保代码质量和系统稳定性的关键环节。测试金字塔模型描述了不同层次测试的理想分布,其中端到端(E2E)测试位于金字塔顶端,用于验证整个系统的功能。NestJS 提供了强大的测试工具,通过 Test.createTestingModule() 可以轻松创建测试模块,结合 supertest 等工具实现完整的 E2E 测试。本文将深入探讨 NestJS 中的测试金字塔,以及如何使用这些工具进行有效的 E2E 测试。
1. 测试金字塔基础概念
1.1 什么是测试金字塔?
测试金字塔是软件测试的一种模型,描述了不同层次测试的数量分布:
typescript
// 测试金字塔结构
// 1. 单元测试 (Unit Tests) - 70%
// - 测试单个函数或类
// - 运行速度快
// - 成本低
// 2. 集成测试 (Integration Tests) - 20%
// - 测试模块间交互
// - 中等运行速度
// - 中等成本
// 3. 端到端测试 (E2E Tests) - 10%
// - 测试完整业务流程
// - 运行速度慢
// - 成本高1.2 NestJS 测试工具
NestJS 提供了完整的测试工具链:
typescript
// NestJS 测试工具
// 1. @nestjs/testing - 核心测试模块
// 2. Test.createTestingModule() - 创建测试模块
// 3. 各种测试装饰器和工具函数
// 4. 与 Jest、Jasmine 等测试框架集成2. 单元测试实现
2.1 服务层单元测试
typescript
// user.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';
describe('UserService', () => {
let service: UserService;
let repository: MockUserRepository;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
{
provide: UserRepository,
useClass: MockUserRepository,
},
],
}).compile();
service = module.get<UserService>(UserService);
repository = module.get(UserRepository);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('createUser', () => {
it('should create a new user', async () => {
const createUserDto = { name: 'John', email: 'john@example.com' };
const createdUser = { id: '1', ...createUserDto };
jest.spyOn(repository, 'create').mockResolvedValue(createdUser);
const result = await service.createUser(createUserDto);
expect(result).toEqual(createdUser);
expect(repository.create).toHaveBeenCalledWith(createUserDto);
});
it('should throw ConflictException when email already exists', async () => {
const createUserDto = { name: 'John', email: 'john@example.com' };
jest.spyOn(repository, 'findByEmail').mockResolvedValue({} as any);
await expect(service.createUser(createUserDto))
.rejects
.toThrow(ConflictException);
});
});
});
// MockUserRepository
class MockUserRepository {
async create(dto: any) {
return Promise.resolve({ id: '1', ...dto });
}
async findByEmail(email: string) {
return Promise.resolve(null);
}
async findById(id: string) {
return Promise.resolve({ id, name: 'John', email: 'john@example.com' });
}
}2.2 控制器层单元测试
typescript
// user.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UserController } from './user.controller';
import { UserService } from './user.service';
describe('UserController', () => {
let controller: UserController;
let service: MockUserService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UserController],
providers: [
{
provide: UserService,
useClass: MockUserService,
},
],
}).compile();
controller = module.get<UserController>(UserController);
service = module.get(UserService);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('create', () => {
it('should create a user', async () => {
const createUserDto = { name: 'John', email: 'john@example.com' };
const createdUser = { id: '1', ...createUserDto };
jest.spyOn(service, 'createUser').mockResolvedValue(createdUser);
const result = await controller.create(createUserDto);
expect(result).toEqual(createdUser);
expect(service.createUser).toHaveBeenCalledWith(createUserDto);
});
});
describe('findOne', () => {
it('should return a user', async () => {
const userId = '1';
const user = { id: userId, name: 'John', email: 'john@example.com' };
jest.spyOn(service, 'getUserById').mockResolvedValue(user);
const result = await controller.findOne(userId);
expect(result).toEqual(user);
expect(service.getUserById).toHaveBeenCalledWith(userId);
});
});
});3. 集成测试实现
3.1 模块间集成测试
typescript
// user-order.integration.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UserModule } from '../user/user.module';
import { OrderModule } from '../order/order.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserController } from '../user/user.controller';
import { OrderController } from '../order/order.controller';
describe('User and Order Integration', () => {
let userController: UserController;
let orderController: OrderController;
let app: INestApplication;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
// 使用真实的模块而不是模拟模块
UserModule,
OrderModule,
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
entities: [User, Order],
synchronize: true,
}),
],
}).compile();
app = module.createNestApplication();
await app.init();
userController = app.get(UserController);
orderController = app.get(OrderController);
});
afterAll(async () => {
await app.close();
});
it('should create user and place order', async () => {
// 1. 创建用户
const createUserDto = { name: 'John', email: 'john@example.com' };
const user = await userController.create(createUserDto);
expect(user).toBeDefined();
expect(user.id).toBeDefined();
// 2. 为用户创建订单
const createOrderDto = {
userId: user.id,
items: [{ productId: '1', quantity: 2, price: 10 }]
};
const order = await orderController.create(createOrderDto);
expect(order).toBeDefined();
expect(order.userId).toBe(user.id);
expect(order.total).toBe(20);
});
});3.2 数据库集成测试
typescript
// database.integration.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '../user/user.entity';
import { UserService } from '../user/user.service';
import { UserRepository } from '../user/user.repository';
describe('Database Integration', () => {
let service: UserService;
let repository: UserRepository;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:', // 内存数据库用于测试
entities: [User],
synchronize: true, // 自动创建表结构
}),
TypeOrmModule.forFeature([User]),
],
providers: [UserService, UserRepository],
}).compile();
service = module.get<UserService>(UserService);
repository = module.get<UserRepository>(UserRepository);
});
it('should save and retrieve user from database', async () => {
const createUserDto = { name: 'John', email: 'john@example.com' };
// 保存用户
const savedUser = await service.createUser(createUserDto);
expect(savedUser.id).toBeDefined();
// 从数据库检索用户
const retrievedUser = await repository.findById(savedUser.id);
expect(retrievedUser).toEqual(savedUser);
});
it('should find user by email', async () => {
const createUserDto = { name: 'John', email: 'john@example.com' };
// 创建用户
await service.createUser(createUserDto);
// 按邮箱查找用户
const foundUser = await repository.findByEmail('john@example.com');
expect(foundUser).toBeDefined();
expect(foundUser.email).toBe('john@example.com');
});
});4. E2E 测试实现
4.1 基础 E2E 测试设置
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();
});
afterEach(async () => {
await app.close();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});4.2 完整的 E2E 测试示例
typescript
// user.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';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '../src/user/user.entity';
describe('UserController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
AppModule,
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
entities: [User],
synchronize: true,
}),
],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('/users (POST)', () => {
it('should create a user', () => {
const createUserDto = {
name: 'John Doe',
email: 'john@example.com',
password: 'password123',
};
return request(app.getHttpServer())
.post('/users')
.send(createUserDto)
.expect(201)
.expect((res) => {
expect(res.body).toMatchObject({
name: 'John Doe',
email: 'john@example.com',
});
expect(res.body.id).toBeDefined();
expect(res.body.password).toBeUndefined(); // 密码不应该返回
});
});
it('should reject invalid email', () => {
const createUserDto = {
name: 'John Doe',
email: 'invalid-email',
password: 'password123',
};
return request(app.getHttpServer())
.post('/users')
.send(createUserDto)
.expect(400);
});
});
describe('/users/:id (GET)', () => {
let userId: string;
beforeEach(async () => {
// 先创建一个用户用于测试
const response = await request(app.getHttpServer())
.post('/users')
.send({
name: 'Jane Doe',
email: 'jane@example.com',
password: 'password123',
})
.expect(201);
userId = response.body.id;
});
it('should get user by id', () => {
return request(app.getHttpServer())
.get(`/users/${userId}`)
.expect(200)
.expect((res) => {
expect(res.body).toMatchObject({
id: userId,
name: 'Jane Doe',
email: 'jane@example.com',
});
});
});
it('should return 404 for non-existent user', () => {
return request(app.getHttpServer())
.get('/users/99999')
.expect(404);
});
});
describe('/users (GET)', () => {
it('should get all users', () => {
return request(app.getHttpServer())
.get('/users')
.expect(200)
.expect((res) => {
expect(Array.isArray(res.body)).toBe(true);
});
});
});
});4.3 复杂业务流程 E2E 测试
typescript
// order.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('Order Flow (e2e)', () => {
let app: INestApplication;
let authToken: string;
let userId: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
it('should complete full order flow', async () => {
// 1. 用户注册
const registerResponse = await request(app.getHttpServer())
.post('/auth/register')
.send({
name: 'John Doe',
email: 'john@example.com',
password: 'password123',
})
.expect(201);
userId = registerResponse.body.id;
// 2. 用户登录
const loginResponse = await request(app.getHttpServer())
.post('/auth/login')
.send({
email: 'john@example.com',
password: 'password123',
})
.expect(200);
authToken = loginResponse.body.access_token;
// 3. 创建订单
const createOrderResponse = await request(app.getHttpServer())
.post('/orders')
.set('Authorization', `Bearer ${authToken}`)
.send({
items: [
{ productId: '1', quantity: 2, price: 10 },
{ productId: '2', quantity: 1, price: 15 },
],
})
.expect(201);
const orderId = createOrderResponse.body.id;
expect(createOrderResponse.body.total).toBe(35);
expect(createOrderResponse.body.status).toBe('pending');
// 4. 获取订单详情
const getOrderResponse = await request(app.getHttpServer())
.get(`/orders/${orderId}`)
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(getOrderResponse.body.id).toBe(orderId);
expect(getOrderResponse.body.userId).toBe(userId);
// 5. 获取用户所有订单
const getUserOrdersResponse = await request(app.getHttpServer())
.get('/orders')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(Array.isArray(getUserOrdersResponse.body)).toBe(true);
expect(getUserOrdersResponse.body.length).toBeGreaterThan(0);
});
});5. 测试工具和最佳实践
5.1 测试数据管理
typescript
// test/fixtures/user.fixture.ts
export class UserFixture {
static createUserData(overrides: Partial<CreateUserDto> = {}): CreateUserDto {
return {
name: 'John Doe',
email: 'john@example.com',
password: 'password123',
...overrides,
};
}
static createAdminUserData(): CreateUserDto {
return {
name: 'Admin User',
email: 'admin@example.com',
password: 'admin123',
role: 'admin',
};
}
}
// test/factories/user.factory.ts
export class UserFactory {
static async create(app: INestApplication, userData?: Partial<CreateUserDto>) {
const createUserDto = UserFixture.createUserData(userData);
const response = await request(app.getHttpServer())
.post('/users')
.send(createUserDto)
.expect(201);
return response.body;
}
static async createMany(app: INestApplication, count: number) {
const users = [];
for (let i = 0; i < count; i++) {
const user = await this.create(app, {
email: `user${i}@example.com`,
name: `User ${i}`,
});
users.push(user);
}
return users;
}
}5.2 测试环境配置
typescript
// test/jest-e2e.json
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"src/**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testTimeout": 30000
}
// package.json scripts
{
"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",
"test:e2e": "jest --config ./test/jest-e2e.json",
"test:e2e:watch": "jest --config ./test/jest-e2e.json --watch"
}
}5.3 测试数据库设置
typescript
// test/setup.ts
import { TypeOrmModule } from '@nestjs/typeorm';
export const testDatabaseOptions = {
type: 'sqlite',
database: ':memory:',
entities: ['src/**/*.entity{.ts,.js}'],
synchronize: true,
dropSchema: true,
logging: false,
};
// 在测试中使用
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
AppModule,
TypeOrmModule.forRoot(testDatabaseOptions),
],
}).compile();
app = module.createNestApplication();
await app.init();
});6. 总结
NestJS 测试金字塔的实现:
- 单元测试:测试单个类或函数,使用模拟对象隔离依赖
- 集成测试:测试模块间交互,使用真实依赖但可能使用内存数据库
- E2E 测试:测试完整业务流程,启动完整应用进行端到端验证
测试工具的核心功能:
- Test.createTestingModule():创建测试模块,支持依赖注入
- supertest:HTTP 测试工具,模拟客户端请求
- 内存数据库:快速测试,避免外部依赖
- 测试装饰器:提供测试专用的依赖注入功能
测试最佳实践:
- 测试分层:按照测试金字塔合理分配测试比例
- 数据隔离:每个测试用例使用独立的测试数据
- 环境配置:为不同测试类型配置合适的环境
- 测试工具:使用专门的测试工具提高效率
- 持续集成:将测试集成到 CI/CD 流程中
通过合理应用 NestJS 的测试工具和遵循测试最佳实践,我们可以:
- 提高代码质量:通过测试发现和修复问题
- 增强系统稳定性:确保功能变更不会破坏现有功能
- 加快开发速度:通过自动化测试减少手动测试时间
- 改善代码设计:可测试的代码通常设计更好
- 降低维护成本:通过测试确保重构安全
至此,我们已经完成了 NestJS 专栏的所有内容,从基础概念到高级特性,从架构设计到最佳实践,全面覆盖了 NestJS 的核心知识点。