
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/lobehubLobeChat 后端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/pglitePostgreSQL 的 WASM 嵌入式版本零配置、随起随用并注册了vector扩展以支撑向量列会遍历migrations目录执行真实的 Drizzle 迁移文件来建表因此表结构完全等价于生产环境由于 PGlite 能力限制迁移脚本中与pg_search、bm25全文搜索相关的 SQL 会被跳过见 getTestDB.ts。模式二node-postgres 真实 PGTEST_SERVER_DB1时启用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_DB1开启需要提供DATABASE_TEST_URL独立的测试库连接串此时会执行全部迁移含 pg_search 相关用于覆盖 PGlite 无法验证的搜索相关场景。两种模式都会把数据库句柄缓存为模块级单例重复调用复用同一实例保证同一进程内测试共用一个 schema 而无需反复建表。建议日常开发跑默认 PGlite 模式即可涉及全文检索等功能再切换到TEST_SERVER_DB1。运行集成测试文档给出三类运行方式映射到当前仓库时的实际用法如下# 运行所有集成测试 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关联和 TopicafterEach只删除用户——由于外键级联删除用户相关的其余数据会被自动清掉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与userIdcreateTestUser(serverDB, userId?)插入一个测试用户直接insert(users).values({ id })createTestAgent(serverDB, userId, agentId?)插入测试 AgentID 以agt_前缀生成插入时onConflictDoNothing()容忍重复createTestTopic(serverDB, userId, topicId?)插入测试主题ID 以tpc_前缀生成同样onConflictDoNothing()cleanupTestUser(serverDB, userId)清理测试用户及全部关联数据只删 user 行依赖外键级联删除代码注释中的一句话点明了清理策略的精髓见 setup.tsDue 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_DB1下确认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),仅供参考