Files
lobehub/docs/development/tests/integration-testing.zh-CN.mdx
René Wang 3dfc86fd0f feat: Update user guide & changelog (#11518)
* feat: Redesign doc

* chore: uopdate site

* chore: uopdate site

* chore: uopdate site

* chore: uopdate site

* chore: uopdate site

* feat: Uopdate content

* chore: New doc

* chore: Update content

* chore: Update content

* chore: add images

* chore: add images

* chore: add images

* chore: add images

* feat: Add more images

* feat: Add more images

* fix: Cannot reach end

* chore: Update content

* chore: Update content

* chore: Update content

* chore: Update content

* chore: Update content

* Revise README content and structure

Updated README to reflect changes in project description and removed outdated notes.

* Revise 'Getting Started' and TOC in README

Updated the 'Getting Started' section and modified the table of contents.

* chore: Update content

* Revise README structure and content

Updated the Getting Started section and removed the Table of Contents. Adjusted the Local Development instructions.

* Remove custom themes section from README

Removed section about custom themes from README.

* Update README.md

* Refine introduction and highlight cloud version

Updated wording for clarity and added recommendation for cloud version.

* chore: Update content

* chore: Update content

* chore: Update content

* chore: Update content

* chore: Update content

* chore: Update content

* chore: Update content

* fix: add missing translation

* 🔀 chore: Move README changes to feat/readme branch

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add missing translation

* chore: update cdn

* docs: add migration guide from v1.x local database to v2.x and update help sections

Signed-off-by: Innei <tukon479@gmail.com>

* fix: add missing translation

* fix: add missing images

* fix: add missing changelogs

* fix: add missing changelogs

* fix: add missing changelogs

* fix: add missing changelogs

* fix: add missing changelogs

* style: update cdn

---------

Signed-off-by: Innei <tukon479@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: canisminor1990 <i@canisminor.cc>
Co-authored-by: Innei <tukon479@gmail.com>
2026-01-26 15:28:33 +08:00

410 lines
10 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: 集成测试指南
description: 了解集成测试的重要性及最佳实践,确保系统模块协同工作。
tags:
- 集成测试
- 测试指南
- 软件测试
- 模块协作
---
# 集成测试指南
## 概述
集成测试验证多个模块协同工作的正确性确保完整的调用链路Router → Service → Model → Database正常运行。
## 为什么需要集成测试?
即使单元测试覆盖率很高80%+),仍可能出现集成问题:
### 常见问题示例
```typescript
// ❌ 问题:参数在调用链中丢失
// Router 层
const messageId = await messageModel.create({
content: 'test',
sessionId: 'xxx',
topicId: 'yyy', // ← 传入了 topicId
});
// Model 层(假设实现有问题)
async create(data) {
return this.db.insert(messages).values({
content: data.content,
sessionId: data.sessionId,
// ❌ 忘记传递 topicId
});
}
// 结果:单元测试通过(因为 mock 了 Model但实际运行时 topicId 丢失
```
### 集成测试能发现的问题
1. **参数传递遗漏**: containerId、threadId、topicId 等在调用链中丢失
2. **数据库约束**: 外键关系、级联删除等在 mock 中无法验证
3. **事务完整性**: 跨表操作的原子性
4. **权限验证**: 跨用户访问控制
5. **真实场景**: 模拟用户的完整操作流程
## 运行集成测试
```bash
# 运行所有集成测试
pnpm test:integration
# 运行特定文件
pnpm vitest tests/integration/routers/message.integration.test.ts
# 监听模式
pnpm vitest tests/integration --watch
# 生成覆盖率报告
pnpm test:integration --coverage
```
## 目录结构
```
tests/integration/
├── README.md # 集成测试说明
├── setup.ts # 通用设置和工具函数
└── routers/ # Router 层集成测试
├── message.integration.test.ts # Message Router 测试
├── session.integration.test.ts # Session Router 测试
├── topic.integration.test.ts # Topic Router 测试
└── chat-flow.integration.test.ts # 完整聊天流程测试
```
## 编写集成测试
### 基本模板
```typescript
// @vitest-environment node
import { eq } from 'drizzle-orm';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { getTestDB } from '@/database/models/__tests__/_util';
import { messages, sessions, users } from '@/database/schemas';
import { LobeHubDatabase } from '@/database/type';
import { messageRouter } from '@/server/routers/lambda/message';
import { cleanupTestUser, createTestContext, createTestUser } from '../setup';
describe('Your Feature Integration Tests', () => {
let serverDB: LobeHubDatabase;
let userId: string;
beforeEach(async () => {
// 1. 获取测试数据库
serverDB = await getTestDB();
// 2. 创建测试用户
userId = await createTestUser(serverDB);
// 3. 准备其他测试数据
// ...
});
afterEach(async () => {
// 清理测试数据
await cleanupTestUser(serverDB, userId);
});
it('should do something', async () => {
// 1. 创建 tRPC caller
const caller = messageRouter.createCaller(createTestContext(userId));
// 2. 执行操作
const result = await caller.someMethod({
/* params */
});
// 3. 验证结果
expect(result).toBeDefined();
// 4. 🔥 关键:从数据库验证
const [dbRecord] = await serverDB.select().from(messages).where(eq(messages.id, result));
expect(dbRecord).toMatchObject({
// 验证所有关键字段
});
});
});
```
### 最佳实践
#### 1. 测试完整的调用链路
```typescript
it('should create message with correct associations', async () => {
const caller = messageRouter.createCaller(createTestContext(userId));
// 执行操作
const messageId = await caller.createMessage({
content: 'Test',
sessionId: testSessionId,
topicId: testTopicId,
});
// ✅ 从数据库验证,而不是只验证返回值
const [message] = await serverDB.select().from(messages).where(eq(messages.id, messageId));
expect(message.sessionId).toBe(testSessionId);
expect(message.topicId).toBe(testTopicId);
expect(message.userId).toBe(userId);
});
```
#### 2. 测试级联操作
```typescript
it('should cascade delete messages when session is deleted', async () => {
const sessionCaller = sessionRouter.createCaller(createTestContext(userId));
const messageCaller = messageRouter.createCaller(createTestContext(userId));
// 创建 session 和 messages
const sessionId = await sessionCaller.createSession({
/* ... */
});
await messageCaller.createMessage({ sessionId /* ... */ });
// 删除 session
await sessionCaller.removeSession({ id: sessionId });
// ✅ 验证相关消息也被删除
const remainingMessages = await serverDB
.select()
.from(messages)
.where(eq(messages.sessionId, sessionId));
expect(remainingMessages).toHaveLength(0);
});
```
#### 3. 测试跨 Router 协作
```typescript
it('should handle complete chat flow', async () => {
const sessionCaller = sessionRouter.createCaller(createTestContext(userId));
const topicCaller = topicRouter.createCaller(createTestContext(userId));
const messageCaller = messageRouter.createCaller(createTestContext(userId));
// 1. 创建 session
const sessionId = await sessionCaller.createSession({
/* ... */
});
// 2. 创建 topic
const topicId = await topicCaller.createTopic({ sessionId /* ... */ });
// 3. 创建 message
const messageId = await messageCaller.createMessage({
sessionId,
topicId,
/* ... */
});
// ✅ 验证完整的关联关系
const [message] = await serverDB.select().from(messages).where(eq(messages.id, messageId));
expect(message.sessionId).toBe(sessionId);
expect(message.topicId).toBe(topicId);
});
```
#### 4. 测试错误场景
```typescript
it('should prevent cross-user access', async () => {
// 用户 A 创建 session
const sessionId = await sessionRouter.createCaller(createTestContext(userA)).createSession({
/* ... */
});
// 用户 B 尝试访问
const callerB = messageRouter.createCaller(createTestContext(userB));
// ✅ 应该抛出错误
await expect(
callerB.createMessage({
sessionId,
content: 'Unauthorized',
}),
).rejects.toThrow();
});
```
#### 5. 测试并发场景
```typescript
it('should handle concurrent operations', async () => {
const caller = messageRouter.createCaller(createTestContext(userId));
// 并发创建多个消息
const promises = Array.from({ length: 10 }, (_, i) =>
caller.createMessage({
content: `Message ${i}`,
sessionId: testSessionId,
}),
);
const messageIds = await Promise.all(promises);
// ✅ 验证所有消息都创建成功且唯一
expect(messageIds).toHaveLength(10);
expect(new Set(messageIds).size).toBe(10);
});
```
### 数据隔离
每个测试用例应该独立,不依赖其他测试:
```typescript
beforeEach(async () => {
// 为每个测试创建新的数据
userId = await createTestUser(serverDB);
testSessionId = await createTestSession(serverDB, userId);
});
afterEach(async () => {
// 清理测试数据
await cleanupTestUser(serverDB, userId);
});
```
### 测试命名
使用清晰的命名描述测试意图:
```typescript
// ✅ 好的命名
it('should create message with correct sessionId and topicId');
it('should cascade delete messages when session is deleted');
it('should prevent cross-user access to messages');
// ❌ 不好的命名
it('test message creation');
it('test delete');
```
## 与单元测试的区别
| 维度 | 单元测试 | 集成测试 |
| ------- | --------- | ------- |
| **范围** | 单个函数 / 类 | 多个模块协作 |
| **依赖** | Mock 外部依赖 | 使用真实依赖 |
| **数据库** | Mock | 真实测试数据库 |
| **速度** | 快(毫秒级) | 慢(秒级) |
| **数量** | 多60% | 少30% |
| **目的** | 验证逻辑正确性 | 验证集成正确性 |
## 测试金字塔
```
/\
/E2E\ ← 10% (关键业务流程)
/------\
/ 集成 \ ← 30% (API 集成测试) ⭐ 本指南重点
/----------\
/ 单元测试 \ ← 60% (已有 80%+)
/--------------\
```
## 覆盖目标
### 优先级 P0必须覆盖
- ✅ 跨层级的 ID 传递sessionId、topicId、containerId、threadId
- ✅ 权限验证(用户只能访问自己的资源)
- ✅ 级联删除(删除 session 时相关数据也删除)
- ✅ 外键约束(不能创建不存在的关联)
### 优先级 P1应该覆盖
- 并发场景(多个请求同时操作)
- 分页查询(正确的数据分页)
- 搜索功能(关键词搜索)
- 批量操作(批量创建 / 删除)
### 优先级 P2可以覆盖
- 统计功能(计数、排名)
- 复杂查询(多条件筛选)
- 性能测试(大量数据场景)
## 调试技巧
### 1. 查看测试数据库状态
```typescript
it('debug test', async () => {
// 执行操作
await caller.createMessage({
/* ... */
});
// 打印数据库状态
const allMessages = await serverDB.select().from(messages);
console.log('All messages:', allMessages);
});
```
### 2. 使用 Drizzle Studio
```bash
# 启动 Drizzle Studio 查看测试数据库
pnpm db:studio
```
### 3. 保留测试数据
```typescript
afterEach(async () => {
// 临时注释掉清理代码,保留数据用于调试
// await cleanupTestUser(serverDB, userId);
});
```
## 常见问题
### Q: 集成测试很慢怎么办?
A:
1. 只测试关键路径,不要过度测试
2. 使用 `test.concurrent` 并行执行独立的测试
3. 优化测试数据准备,避免重复创建
### Q: 测试之间相互影响怎么办?
A:
1. 确保每个测试使用独立的 userId
2. 在 `afterEach` 中彻底清理数据
3. 使用事务隔离(如果数据库支持)
### Q: 如何测试需要认证的 API
A: 使用 `createTestContext(userId)` 创建带认证信息的上下文:
```typescript
const caller = messageRouter.createCaller(createTestContext(userId));
```
## 参考资料
- [Vitest 文档](https://vitest.dev/)
- [Drizzle ORM 文档](https://orm.drizzle.team/)
- [tRPC 测试指南](https://trpc.io/docs/server/testing)
- [测试金字塔](https://martinfowler.com/articles/practical-test-pyramid.html)
## 贡献
欢迎补充更多集成测试用例!请参考现有测试文件的风格。