UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

400 lines (298 loc) 10.5 kB
# 集成测试指南 ## 概述 集成测试验证多个模块协同工作的正确性,确保完整的调用链路(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 { LobeChatDatabase } from '@/database/type'; import { messageRouter } from '@/server/routers/lambda/message'; import { cleanupTestUser, createTestContext, createTestUser } from '../setup'; describe('Your Feature Integration Tests', () => { let serverDB: LobeChatDatabase; 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) ## 贡献 欢迎补充更多集成测试用例!请参考现有测试文件的风格。