@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
text/mdx
集成测试验证多个模块协同工作的正确性,确保完整的调用链路(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/
├── message.integration.test.ts
├── session.integration.test.ts
├── topic.integration.test.ts
└── 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({
// 验证所有关键字段
});
});
});
```
```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);
});
```
```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);
});
```
```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);
});
```
```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();
});
```
```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%+)
/--------------\
```
- ✅ 跨层级的 ID 传递(sessionId、topicId、containerId、threadId)
- ✅ 权限验证(用户只能访问自己的资源)
- ✅ 级联删除(删除 session 时相关数据也删除)
- ✅ 外键约束(不能创建不存在的关联)
- 并发场景(多个请求同时操作)
- 分页查询(正确的数据分页)
- 搜索功能(关键词搜索)
- 批量操作(批量创建 / 删除)
- 统计功能(计数、排名)
- 复杂查询(多条件筛选)
- 性能测试(大量数据场景)
```typescript
it('debug test', async () => {
// 执行操作
await caller.createMessage({
/* ... */
});
// 打印数据库状态
const allMessages = await serverDB.select().from(messages);
console.log('All messages:', allMessages);
});
```
```bash
pnpm db:studio
```
```typescript
afterEach(async () => {
// 临时注释掉清理代码,保留数据用于调试
// await cleanupTestUser(serverDB, userId);
});
```
A:
1. 只测试关键路径,不要过度测试
2. 使用 `test.concurrent` 并行执行独立的测试
3. 优化测试数据准备,避免重复创建
A:
1. 确保每个测试使用独立的 userId
2. 在 `afterEach` 中彻底清理数据
3. 使用事务隔离(如果数据库支持)
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)
欢迎补充更多集成测试用例!请参考现有测试文件的风格。