0%

NestJS 框架下的三种测试类型对比分析

概述

本文以NestJS框架为例,深入对比分析单元测试、集成测试和端到端(E2E)测试的核心区别,帮助开发者在实际项目中选择合适的测试策略。

1. 三种测试类型的核心区别

1.1 定义与测试范围对比

测试类型 定义 测试范围 NestJS中的体现
单元测试 测试单个函数、方法或类的逻辑 最小可测试单元 Service方法、Controller方法、Pipe、Guard等
集成测试 测试多个模块间的交互 模块间接口和数据流 Service与Repository交互、Module间通信
E2E测试 测试完整的用户场景 整个应用流程 HTTP请求到响应的完整链路

1.2 执行特性对比

特性 单元测试 集成测试 E2E测试
执行速度 极快(毫秒级) 中等(秒级) 较慢(分钟级)
隔离性 完全隔离 部分隔离 无隔离
依赖处理 Mock所有依赖 Mock外部依赖 使用真实依赖
环境要求 无需外部环境 需要部分真实环境 需要完整环境

2. NestJS框架下的具体实现对比

2.1 单元测试示例

测试目标:UserService中的创建用户方法

1
2
3
4
5
6
7
8
9
10
11
12
13
// user.service.ts
@Injectable()
export class UserService {
constructor(private userRepository: UserRepository) {}

async createUser(userData: CreateUserDto): Promise {
const existingUser = await this.userRepository.findByEmail(userData.email);
if (existingUser) {
throw new ConflictException('User already exists');
}
return this.userRepository.create(userData);
}
}

单元测试实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// user.service.spec.ts
describe('UserService', () => {
let service: UserService;
let mockRepository: jest.Mocked;

beforeEach(async () => {
const mockRepo = {
findByEmail: jest.fn(),
create: jest.fn(),
};

const module = await Test.createTestingModule({
providers: [
UserService,
{ provide: UserRepository, useValue: mockRepo },
],
}).compile();

service = module.get(UserService);
mockRepository = module.get(UserRepository);
});

it('should create user when email not exists', async () => {
// Arrange
const userData = { email: '[email protected]', name: 'Test User' };
mockRepository.findByEmail.mockResolvedValue(null);
mockRepository.create.mockResolvedValue({ id: 1, ...userData });

// Act
const result = await service.createUser(userData);

// Assert
expect(mockRepository.findByEmail).toHaveBeenCalledWith(userData.email);
expect(mockRepository.create).toHaveBeenCalledWith(userData);
expect(result).toEqual({ id: 1, ...userData });
});
});

特点分析:

  • 完全隔离:Mock了UserRepository依赖

  • 快速执行:无需数据库连接

  • 精确验证:只测试业务逻辑

  • 无法发现:Repository接口变更问题

2.2 集成测试示例

测试目标:UserService与真实数据库的交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// user.integration.spec.ts
describe('UserService Integration', () => {
let app: INestApplication;
let service: UserService;
let repository: Repository;

beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
entities: [User],
synchronize: true,
}),
TypeOrmModule.forFeature([User]),
],
providers: [UserService, UserRepository],
}).compile();

app = module.createNestApplication();
await app.init();

service = module.get(UserService);
repository = module.get>(getRepositoryToken(User));
});

beforeEach(async () => {
await repository.clear(); // 清理测试数据
});

it('should create user and save to database', async () => {
// Arrange
const userData = { email: '[email protected]', name: 'Test User' };

// Act
const createdUser = await service.createUser(userData);

// Assert
expect(createdUser.id).toBeDefined();
expect(createdUser.email).toBe(userData.email);

// 验证数据确实保存到数据库
const savedUser = await repository.findOne({ where: { email: userData.email } });
expect(savedUser).toBeTruthy();
});

it('should throw conflict when user already exists', async () => {
// Arrange
const userData = { email: '[email protected]', name: 'Test User' };
await repository.save(userData); // 预先创建用户

// Act & Assert
await expect(service.createUser(userData)).rejects.toThrow(ConflictException);
});
});

特点分析:

  • 真实交互:使用真实数据库操作

  • 接口验证:能发现Service与Repository接口问题

  • 数据验证:确认数据正确保存

  • 执行较慢:需要数据库操作

  • 环境依赖:需要配置测试数据库

2.3 E2E测试示例

测试目标:完整的用户注册API流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// user.e2e-spec.ts
describe('User E2E', () => {
let app: INestApplication;
let httpServer: any;

beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [AppModule], // 导入完整应用模块
}).compile();

app = module.createNestApplication();
app.useGlobalPipes(new ValidationPipe()); // 应用全局管道
await app.init();

httpServer = app.getHttpServer();
});

beforeEach(async () => {
// 清理测试数据
const userRepository = app.get>(getRepositoryToken(User));
await userRepository.clear();
});

it('/users (POST) - should create user successfully', async () => {
// Arrange
const userData = {
email: '[email protected]',
name: 'Test User',
password: 'password123'
};

// Act
const response = await request(httpServer)
.post('/users')
.send(userData)
.expect(201);

// Assert
expect(response.body).toMatchObject({
id: expect.any(Number),
email: userData.email,
name: userData.name,
});
expect(response.body.password).toBeUndefined(); // 密码不应返回
});

it('/users (POST) - should return 409 when user exists', async () => {
// Arrange
const userData = {
email: '[email protected]',
name: 'Test User',
password: 'password123'
};

// 先创建用户
await request(httpServer)
.post('/users')
.send(userData)
.expect(201);

// Act & Assert
await request(httpServer)
.post('/users')
.send(userData)
.expect(409);
});

it('/users (POST) - should validate input data', async () => {
// Act & Assert
await request(httpServer)
.post('/users')
.send({
email: 'invalid-email', // 无效邮箱
name: '', // 空名称
})
.expect(400);
});
});

特点分析:

  • 完整流程:测试HTTP请求到数据库的完整链路

  • 真实场景:模拟用户实际操作

  • 全面验证:包含验证、异常处理、响应格式等

  • 执行最慢:启动完整应用

  • 维护成本高:接口变更需要同步更新

3. 在NestJS项目中的选择策略

3.1 测试金字塔在NestJS中的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
        E2E Tests (10%)
┌─────────────────┐
│ 核心业务流程 │
└─────────────────┘

Integration Tests (20%)
┌───────────────────────┐
│ Service ↔ Repository │
│ Module间交互 │
└───────────────────────┘

Unit Tests (70%)
┌─────────────────────────────┐
│ Service方法、Controller方法 │
│ Pipe、Guard、Interceptor │
│ 工具函数、业务逻辑 │
└─────────────────────────────┘

3.2 具体应用建议

单元测试重点关注:

  • Service中的业务逻辑方法

  • Controller中的参数处理和响应格式化

  • 自定义Pipe的数据转换逻辑

  • Guard的权限验证逻辑

  • 工具函数和算法

集成测试重点关注:

  • Service与Repository的数据操作

  • Module间的依赖注入

  • 第三方服务的集成(如Redis、消息队列)

  • 数据库事务处理

E2E测试重点关注:

  • 用户注册/登录流程

  • 核心业务操作流程

  • 权限控制的完整验证

  • 错误处理的用户体验

4. 实际项目中的测试配置

4.1 package.json测试脚本

1
2
3
4
5
6
7
8
9
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:integration": "jest --config ./test/jest-integration.json",
"test:e2e": "jest --config ./test/jest-e2e.json"
}
}

4.2 Jest配置文件

单元测试配置 (jest.config.js):

1
2
3
4
5
6
7
8
9
10
module.exports = {
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: 'src',
testRegex: '.*\\.spec\\.ts$', // 只匹配 .spec.ts 文件
transform: { '^.+\\.(t|j)s$': 'ts-jest' },
collectCoverageFrom: ['**/*.(t|j)s'],
coverageDirectory: '../coverage',
testEnvironment: 'node',
testPathIgnorePatterns: ['.*\\.integration\\.spec\\.ts$'], // 排除集成测试
};

集成测试配置 (test/jest-integration.json):

1
2
3
4
5
6
7
8
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": "../src",
"testEnvironment": "node",
"testRegex": ".*\\.integration\\.spec\\.ts$",
"transform": { "^.+\\.(t|j)s$": "ts-jest" },
"setupFilesAfterEnv": ["/../test/integration-setup.ts"]
}

E2E测试配置 (test/jest-e2e.json):

1
2
3
4
5
6
7
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": { "^.+\\.(t|j)s$": "ts-jest" }
}

集成测试环境设置 (test/integration-setup.ts):

1
2
3
4
5
6
7
8
9
10
11
import { Test } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';

// 全局集成测试配置
beforeAll(async () => {
// 设置测试数据库连接等
});

afterAll(async () => {
// 清理资源
});

5. 总结

在NestJS框架下,三种测试类型各有其适用场景:

选择单元测试当:

  • 验证复杂业务逻辑

  • 需要快速反馈

  • 测试覆盖率要求高

选择集成测试当:

  • 验证数据库操作

  • 测试模块间交互

  • 确保接口契约正确

选择E2E测试当:

  • 验证关键业务流程

  • 确保用户体验

  • 发布前的最终验证

合理的测试策略应该是70%单元测试 + 20%集成测试 + 10%E2E测试,这样既能保证代码质量,又能控制测试维护成本。

6. 扩展:其他测试类型的补充说明

6.1 测试分类的两个维度

虽然本文重点讨论单元测试、集成测试和E2E测试,但在实际项目中还存在其他测试类型。理解测试的分类维度很重要:

按执行方式分类:

  • 自动化测试:通过代码自动执行(本文重点)

  • 工具驱动测试:使用专门工具执行

  • 手工测试:需要人工操作

按测试目标分类:

  • 功能性测试:验证功能是否正确实现

  • 非功能性测试:验证性能、安全、可用性等

6.2 代码驱动的自动化测试 vs 其他测试类型

本文讨论的三种测试(代码驱动)

1
2
3
4
5
6
7
8
// 完全通过代码自动执行
describe('UserService', () => {
it('should create user when email not exists', async () => {
// 自动化的断言检查
expect(result.email).toBe('[email protected]');
expect(mockRepository.create).toHaveBeenCalledWith(userData);
});
});

特点:

  • ✅ 完全自动化执行

  • ✅ 可集成到CI/CD流程

  • ✅ 开发过程中持续运行

  • ✅ 快速反馈和问题定位

其他测试类型(工具/人工驱动)

API测试(工具驱动):

1
2
3
4
5
6
7
# 使用Postman/Newman
newman run api-tests.postman_collection.json

# 使用专门的API测试工具
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","name":"Test User"}'

性能测试(工具驱动):

1
2
3
4
5
# 使用Artillery进行负载测试
artillery run load-test.yml

# 使用JMeter
jmeter -n -t performance-test.jmx

安全测试(工具驱动):

1
2
3
4
5
6
# 依赖漏洞扫描
npm audit
snyk test

# 代码安全扫描
eslint --ext .ts src/ --config .eslintrc-security.js

6.3 完整的测试策略配置

package.json中的完整测试脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"scripts": {
// 代码驱动的自动化测试(本文重点)
"test": "jest",
"test:unit": "jest --config ./test/jest-unit.json",
"test:integration": "jest --config ./test/jest-integration.json",
"test:e2e": "jest --config ./test/jest-e2e.json",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",

// 工具驱动的测试
"test:api": "newman run ./test/api-tests.postman_collection.json",
"test:performance": "artillery run ./test/load-test.yml",
"test:security": "npm audit && snyk test",
"test:lint": "eslint src/**/*.ts",

// 综合测试脚本
"test:all": "npm run test:unit && npm run test:integration && npm run test:e2e",
"test:ci": "npm run test:lint && npm run test:security && npm run test:all"
}
}

6.4 测试策略的完整图景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
代码驱动的自动化测试(开发者日常)    其他测试类型(专项/阶段性)

E2E Tests (10%) Manual Testing
┌─────────────────┐ ┌─────────────────┐
│ 关键业务流程 │ │ 可用性、探索性 │
└─────────────────┘ └─────────────────┘

Integration Tests (20%) Tool-based Testing
┌─────────────────────┐ ┌─────────────────┐
│ 模块间交互验证 │ │ 性能、安全扫描 │
└─────────────────────┘ └─────────────────┘

Unit Tests (70%) Static Analysis
┌─────────────────────┐ ┌─────────────────┐
│ 业务逻辑验证 │ │ 代码质量检查 │
└─────────────────────┘ └─────────────────┘

6.5 为什么本文重点讲代码驱动的测试

开发者日常最需要的技能:

  1. 高频使用:每天开发过程中都要编写和运行

  2. 即时反馈:能在编码时立即发现问题

  3. CI/CD集成:可以自动化集成到部署流程

  4. 成本效益:一次编写,持续受益

其他测试类型的特点:

  • 执行频率较低:通常在特定阶段执行(如发布前)

  • 专门工具:需要学习和配置专门的测试工具

  • 专业团队:更多由QA或运维团队负责

  • 环境要求:需要特殊的测试环境和数据

6.6 实际项目中的应用建议

开发阶段(每日):

  • 单元测试:验证业务逻辑

  • 集成测试:验证模块交互

  • 代码质量检查:ESLint、Prettier

集成阶段(每次提交):

  • E2E测试:验证关键流程

  • API测试:验证接口契约

  • 安全扫描:检查依赖漏洞

发布阶段(版本发布前):

  • 性能测试:验证系统负载能力

  • 兼容性测试:多浏览器/设备验证

  • 手工测试:用户体验验证

通过这种分层的测试策略,既保证了开发效率,又确保了产品质量。代码驱动的自动化测试构成了质量保障的基础,而其他测试类型则在特定场景下提供补充验证。