LobeChat 后端集成测试指南:基于真实数据库的 tRPC Router 全链路验证实践
【免费下载链接】lobehub🤯 LobeHub is your Chief Agent Operator, organizing your agents into 7×24 operations by hiring, scheduling, and reporting on your entire AI team.项目地址: https://gitcode.com/GitHub_Trending/lo/lobehub
LobeChat 后端(apps/server)采用分层架构:tRPC Router负责对外暴露业务接口,Service承载业务逻辑,Model与Drizzle ORM打交道,最终读写PostgreSQL。为了验证这条完整调用链在多模块协同下依然正确,团队在apps/server/src/routers/lambda/__tests__/integration/目录维护了一套集成测试,它把「Router → Service → Model → 真实数据库」串起来跑,覆盖消息、会话、主题、Agent 执行、文件、搜索等大量业务场景。本文以仓库内的 集成测试 README 为主体,结合其 setup 工具、真实测试用例 与底层 getTestDB 实现,帮助你掌握这类测试的定位、运行方式、编写规范与数据库环境搭建原理,并能直接应用到 LobeChat 相关 Router 的贡献开发中。
目录结构与文件定位
README 中的目录树展示的是一套「按通用测试规范」组织的结构,而当前仓库里集成测试的实际落点已经迁移到 Router 就近放置,方便与路由实现对照阅读。真实目录结构如下:
apps/server/src/routers/lambda/__tests__/integration/ ├── README.md # 本指南(集成测试说明文档) ├── setup.ts # 集成测试通用工具(上下文、测试用户/Agent/Topic 工厂) ├── helpers/ # 通用辅助(如 openaiMock.ts 等服务 mock) ├── aiAgent/ # Agent 执行类集成测试(execAgent、execAgents、serverCallAgent 等) ├── message.integration.test.ts # 消息 Router 集成测试 ├── topic.integration.test.ts # 主题 Router 集成测试 ├── agentDocumentVfs.integration.test.ts ├── agentEval.integration.test.ts ├── oauthApp.integration.test.ts ├── project.integration.test.ts ├── task.integration.test.ts └── ... # 其余 *.integration.test.ts可以看出,README 中举例的message.integration.test.ts、topic.integration.test.ts均已落地,并在此之上扩展出了 Agent 执行链路(aiAgent/子目录)等多个主题。每个.integration.test.ts文件都与同目录__tests__上一级的同名 Router(如 message.ts、topic.ts)一一对应,便于对照源码阅读。
什么是集成测试:与单元测试的边界
文档对集成测试给出了清晰的定义——验证多个模块协同工作的正确性,并把它与单元测试做了区分:
| 维度 | 单元测试 | 集成测试 |
|---|---|---|
| 测试对象 | 单个函数 / 类 | 完整的调用链路(Router → Service → Model → Database) |
| 依赖隔离 | 使用 mock 隔离依赖 | 使用真实数据库 |
| 验证重点 | 单一模块的输入输出 | 模块间协作、参数透传、数据库约束 |
在 LobeChat 的语境下,「真实的调用链路」可以进一步具象化为:通过 tRPC 的router.createCaller(context)以「服务端内部调用」的方式直接触发 Router 过程,Router 内部调用 Service/Model,最终落到测试数据库并回读验证。以 message.integration.test.ts 中createMessage用例为例,测试并不 mock 掉业务层,而是从数据库中查出刚写入的记录来断言:
const caller = messageRouter.createCaller(createTestContext(userId)); const result = await caller.createMessage({ content: 'Test message', role: 'user', sessionId: testSessionId, topicId: testTopicId, }); // 从数据库回读,验证 sessionId 被正确解析为 agentId 后落库 const [createdMessage] = await serverDB .select() .from(messages) .where(eq(messages.id, result.id)); expect(createdMessage).toMatchObject({ id: result.id, agentId: testAgentId, // sessionId 在链路中被解析为 agentId 存储 topicId: testTopicId, userId, content: 'Test message', role: 'user', });这个断言非常具有代表性:它验证的是业务链路的「副作用」(sessionId到agentId的归属解析、关联关系的正确落库),这类行为在纯单元测试中是难以被测到的。
为什么需要集成测试
文档强调:即使单元测试覆盖率很高(80%+),仍可能出现集成问题。它列举了四类典型痛点,这些也正是集成测试的着力点:
- 参数传递遗漏:如
containerId、threadId、groupId这类跨层参数在多层调用链中容易被「遗忘」,单测各层都通过、合起来却丢了参数; - 数据库约束:外键关系、级联删除、唯一索引、非空约束等数据库层面的强约束,在 mock 中完全无法验证;
- 事务完整性:跨表操作的原子性(全部成功或全部回滚)需要真实事务才能检验;
- 真实场景:模拟用户的完整操作流程,例如「先建会话 → 再开主题 → 发消息」这种真实顺序操作。
从仓库现状看,这套认知已经被完整贯彻:message.integration.test.ts的测试目标注释明确写着「验证完整的 tRPC 调用链(Router → Model → Database)」「确保 sessionId、topicId、groupId 等参数被正确传递」「验证数据库约束与关联」(见 message.integration.test.ts),正是对文档观点最直接的落地印证。
数据库环境:双模式 test DB 的实现原理
文档第一条最佳实践是「使用真实数据库环境」,并给出取数据库句柄的代码:
const serverDB = await getTestDB();需要说明的是:README 中该示例沿用了旧的本地 alias 写法,而当前仓库的实际导入路径为包级导出@lobechat/database/test-utils(由 packages/database/tests/test-utils.ts 重新导出),真实的集成测试如 message.integration.test.ts 的写法为:
import { getTestDB } from '@lobechat/database/test-utils'; beforeEach(async () => { serverDB = await getTestDB(); });getTestDB的底层实现位于 packages/database/src/core/getTestDB.ts,它支持两种真实数据库模式,理解它有助于把握集成测试的运行前提:
模式一:PGlite 内存模式(默认,无需任何外部服务)
const isServerDBMode = process.env.TEST_SERVER_DB === '1'; if (!isServerDBMode) { const pglite = new PGlite({ extensions: { vector } }); testClientDB = pgliteDrizzle({ client: pglite, schema }); // 遍历 migrations 目录逐条执行迁移建表 }- 默认使用
@electric-sql/pglite(PostgreSQL 的 WASM 嵌入式版本),零配置、随起随用,并注册了vector扩展以支撑向量列; - 会遍历
migrations目录执行真实的 Drizzle 迁移文件来建表,因此表结构完全等价于生产环境; - 由于 PGlite 能力限制,迁移脚本中与
pg_search、bm25(全文搜索相关)的 SQL 会被跳过,见 getTestDB.ts。
模式二:node-postgres 真实 PG(TEST_SERVER_DB=1时启用)
if (isServerDBMode) { const connectionString = serverDBEnv.DATABASE_TEST_URL; if (!connectionString) throw new Error('DATABASE_TEST_URL is not set'); const client = new NodePool({ connectionString }); testServerDB = nodeDrizzle(client, { schema }); await nodeMigrate(testServerDB, { migrationsFolder }); // 执行完整迁移 }- 通过环境变量
TEST_SERVER_DB=1开启,需要提供DATABASE_TEST_URL(独立的测试库连接串); - 此时会执行全部迁移(含 pg_search 相关),用于覆盖 PGlite 无法验证的搜索相关场景。
两种模式都会把数据库句柄缓存为模块级单例,重复调用复用同一实例,保证同一进程内测试共用一个 schema 而无需反复建表。建议:日常开发跑默认 PGlite 模式即可,涉及全文检索等功能再切换到TEST_SERVER_DB=1。
运行集成测试
文档给出三类运行方式,映射到当前仓库时的实际用法如下:
# 运行所有集成测试 pnpm test:integration # 运行特定文件(把 tests/integration/... 对应到当前真实目录) pnpm vitest apps/server/src/routers/lambda/__tests__/integration/message.integration.test.ts # 监听模式(配合 --watch 在改动时自动重跑) pnpm vitest apps/server/src/routers/lambda/__tests__/integration --watch针对本仓库有两处需要留意:
- 当前各 integration 测试文件首行均声明
// @vitest-environment node,确保跑在 Node 环境而非默认的 jsdom 环境中,见 message.integration.test.ts; - 若要针对单个业务域(例如 Agent 执行)跑批,可直接指定子目录:
pnpm vitest apps/server/src/routers/lambda/__tests__/integration/aiAgent。
另外提醒:集成测试基于真实数据库,运行前请确认环境中对应模式的数据库可用(默认 PGlite 模式则无需任何准备);如果所用仓库版本未定义test:integration脚本,可直接以pnpm vitest加目录/文件参数的方式执行。
编写集成测试的最佳实践
1. 使用真实数据库环境
不要 mock 掉数据访问层。通过getTestDB()拿到与生产同构(同一套 schema 与迁移)的真实数据库实例,才能让外键、唯一约束、级联删除真正生效:
import { getTestDB } from '@lobechat/database/test-utils'; let serverDB: LobeChatDatabase; beforeEach(async () => { serverDB = await getTestDB(); });2. 每个测试用例独立
用例之间互不依赖:beforeEach准备自己的数据,afterEach清理数据。文档给出的是对users表的插入与删除,仓库中这套逻辑已沉淀为公共工具(见下文「公共工具函数」一节),实际测试用例如 message.integration.test.ts 所示,在beforeEach中依次创建用户、Agent、会话、agentsToSessions关联和 Topic,afterEach只删除用户——由于外键级联删除,用户相关的其余数据会被自动清掉:
afterEach(async () => { await cleanupTestUser(serverDB, userId); // 靠外键级联删除清空该用户全部关联数据 });3. 测试完整的调用链路
文档强调应「通过 Router 入口发起、再到数据库验证结果」,而不是只测 Service 方法。推荐形态是:messageRouter.createCaller(createTestContext(userId))构造带身份上下文的调用器 → 调用caller.xxx()→ 用 SQL 回读断言数据库最终状态。这种写法的价值在于,Router 层的入参解析、鉴权前置校验、参数归一化逻辑都被真实执行,任何一环的缺陷都会让测试失败。
4. 验证关键路径
文档建议优先覆盖以下高风险点,这些正是历史上最容易在多层协作中出错的地方:
- 跨层级的 ID 传递:
sessionId、topicId、threadId、containerId、groupId在 Router → Service → Model 间逐层透传是否正确; - 权限验证:用户只能读写自己的数据,越权访问应被拒绝;
- 并发场景:多请求并发写入、幂等性等;
- 错误处理:非法输入、引用不存在的记录时是否抛出预期异常。
在 message.integration.test.ts 中可以看到threadId透传这类用例——先建threads记录,再携带threadId调createMessage,最后回读断言消息确实挂在了该 thread 下;而「sessionId 不存在时应报错」的用例(见 message.integration.test.ts)则覆盖了错误路径。文件中甚至保留了it.skip('should fail when topicId does not belong to sessionId', ...),注释说明该校验当前代码尚未强制实施,展示了「用集成测试记录已知行为缺口」的务实做法。
公共工具函数:setup.ts 的作用
文档目录树中提到的setup.ts/utils.ts在仓库里统一收敛为 setup.ts。其导出的工具函数构成了所有集成测试的公共基座:
| 函数 | 作用 | 关键实现细节 |
|---|---|---|
createTestContext(userId?) | 构造 tRPC 调用所需的鉴权上下文 | 未传userId时自动uuid(),内含jwtPayload.userId与userId |
createTestUser(serverDB, userId?) | 插入一个测试用户 | 直接insert(users).values({ id }) |
createTestAgent(serverDB, userId, agentId?) | 插入测试 Agent | ID 以agt_前缀生成,插入时onConflictDoNothing()容忍重复 |
createTestTopic(serverDB, userId, topicId?) | 插入测试主题 | ID 以tpc_前缀生成,同样onConflictDoNothing() |
cleanupTestUser(serverDB, userId) | 清理测试用户及全部关联数据 | 只删 user 行,依赖外键级联删除 |
代码注释中的一句话点明了清理策略的精髓(见 setup.ts):Due to foreign key cascade deletion, only the user needs to be deleted——这正是「真实数据库约束让清理变简单」的典型例子。若在 mock 环境里,清理逻辑反而要手工模拟级联,无从体会真实约束的价值。
外部依赖的 mock 边界:哪些仍需要 mock?
虽然集成测试强调「真实」,但对进程外基础设施仍要做必要隔离。观察 message.integration.test.ts 可归纳出两条实用边界:
// 1) Mock 掉会初始化云服务连接的文件服务(S3) vi.mock('@/server/services/file', () => ({ FileService: vi.fn().mockImplementation(() => ({ getFullFileUrl: vi.fn().mockResolvedValue('mock-url'), deleteFile: vi.fn().mockResolvedValue(undefined), deleteFiles: vi.fn().mockResolvedValue(undefined), })), })); // 2) 把“获取生产 DB”的函数替换为返回测试 DB let testDB: LobeChatDatabase; vi.mock('@/database/core/db-adaptor', () => ({ getServerDB: vi.fn(() => testDB), }));- 对象存储 / 第三方云服务(如 FileService 背后的 S3)属于无法在单测环境真实初始化的外部设施,将其 mock 成「返回固定 URL / 空实现」是合理取舍;
- 数据库适配层入口
getServerDB被替换为返回测试实例,但替换之后仍是真实的 SQL 执行——测试数据库本身没有被 mock。
简言之:数据库要真实,云服务可隔离。这条实践被目录中多个测试文件复用(如agentEval、aiAgent等测试同样从@lobechat/database/test-utils取真实 DB),说明它已是团队约定俗成的测试基建。
测试覆盖目标与注意事项
文档最后给出了覆盖目标与运维提醒,二者分别从「测什么」与「怎么控成本」两个角度约束测试策略:
覆盖目标
- API 层集成测试:30%
- 关键业务流程:100%
- 错误场景:主要路径覆盖
含义是:不是所有代码都要写集成测试,而是把宝贵的真实数据库执行资源优先投入到关键业务流程(100%)和主要错误路径上;对覆盖面广但较薄的 API 层,达到 30% 即可作为底线目标。
注意事项
- 集成测试比单元测试慢,不要过度使用——能用单测锁定的纯函数逻辑留在单测,集成测试聚焦链路与数据;
- 保持测试数据隔离,避免测试间相互影响——每个用例独立建用户、独立清理是基本纪律;
- 使用有意义的测试数据,便于调试——像
'Test message in thread'这类带语义的文案,比随机字符串更容易在失败时快速定位; - 测试失败时,先检查数据库状态——插入失败、外键冲突、迁移未执行等数据库层问题,是集成测试失败的头号来源,配合
getTestDB的两种模式可逐一排查(PGlite 下关注被跳过的 pg_search 迁移,TEST_SERVER_DB=1下确认DATABASE_TEST_URL与迁移完整性)。
结语
LobeChat 的集成测试体系回答了后端开发中最朴素也最难的一个问题:各层各自正确,串起来是否依然正确?通过「Router 入口发起 + 真实数据库回读断言」的范式,它让参数透传、外键级联、事务与并发等跨模块问题在提交前就能暴露;而getTestDB的双模式设计(PGlite 快速迭代 + node-postgres 全量验证)则把「接近生产的真实」与「随手可跑的轻量」统一了起来。如果你正在为 LobeChat 贡献新的 Router 逻辑,不妨对照 message.integration.test.ts 的模式,用一组真实的端到端数据把关键链路钉死在数据库里。
【免费下载链接】lobehub🤯 LobeHub is your Chief Agent Operator, organizing your agents into 7×24 operations by hiring, scheduling, and reporting on your entire AI team.项目地址: https://gitcode.com/GitHub_Trending/lo/lobehub
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考