ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Cherry Studio 数据库测试指南:基于 setupTestDatabase 与生产迁移的 SQLite 主进程测试

Cherry Studio 数据库测试指南:基于 setupTestDatabase 与生产迁移的 SQLite 主进程测试 Cherry Studio 数据库测试指南基于 setupTestDatabase 与生产迁移的 SQLite 主进程测试【免费下载链接】cherry-studioAI productivity studio with smart chat, autonomous agents, and 300 assistants. Unified access to frontier LLMs项目地址: https://gitcode.com/GitHub_Trending/ch/cherry-studio本文是一份面向 Cherry Studio 主进程 SQLite 数据层的实战测试指南系统讲解统一测试装置test harnesssetupTestDatabase()的用法、生命周期机制、适用边界与常见反模式。读完本文你将掌握如何为 Service、Handler、Seeder 与迁移代码编写真实读写 SQLite 的 Vitest 用例理解它如何通过生产application.get(DbService).getDb()路径注入真实数据库并学会规避 better-sqlite3 ABI、FTS5 触发器与并发等隐藏陷阱。TL;DR三行接入真实数据库任何读写 SQLite 的服务Service、处理器Handler、种子数据器Seeder或迁移Migration都应使用来自test-helpers/db的setupTestDatabase()。它会在 Vitest 生命周期内挂接一个真实、隔离、基于文件的 SQLite 数据库并通过生产的application.get(DbService).getDb()路径暴露它。你不需要mockapplication不需要手写任何CREATE TABLESQL也不需要再使用vi.mock(node:fs, importOriginal)这种逃生通道。import { setupTestDatabase } from test-helpers/db import { messageService } from data/services/MessageService import { messageTable } from data/db/schemas/message import { eq } from drizzle-orm describe(MessageService, () { const dbh setupTestDatabase() it(persists a message, async () { const msg await messageService.create({ topicId: t1, role: user, ... }) const [row] dbh.db .select() .from(messageTable) .where(eq(messageTable.id, msg.id)) expect(row).toMatchObject({ role: user }) }) })装置入口定义在 tests/helpers/db/testDatabase.ts并经由 tests/helpers/db/index.ts 统一导出setupTestDatabase、TestDatabaseHandle、TestDatabaseOptions以及消息树辅助函数。Harness 到底做了什么六步初始化在测试文件中的第一个用例执行前harness 的beforeAll钩子会完成如下初始化对应 testDatabase.ts 的实现创建唯一临时目录通过mkdtempSync(join(tmpdir(), cs-test-db-))在os.tmpdir()下生成形如cs-test-db-xxxxxx的独立目录保证并发与多文件之间互不干扰。打开文件型 SQLite 并暴露原生连接在tmp/test.db位置用 better-sqlite3 打开数据库同时构造 Drizzle 实例drizzle({ client: sqlite, casing: snake_case })作为dbh.db并把原生连接作为dbh.sqlite提供给需要绕过 Drizzle 直接执行 SQL/PRAGMA 的测试。执行生产迁移通过与DbService.onInit完全相同的applyMigrations()函数运行 migrations/sqlite-drizzle/ 下的生产迁移文件并执行项目中 Drizzle 无法管理的CUSTOM_SQL_STATEMENTSFTS5 虚拟表、触发器。迁移函数定义在 src/main/data/db/applyMigrations.ts它是实时库DbService.onInit、测试库harness、备份恢复管线detached work.sqlite 前向迁移三方共享的唯一定点其内部会临时关闭外键约束以规避 drizzle-orm migrator 在单事务内PRAGMA foreign_keysOFF失效的问题并在迁移后执行PRAGMA foreign_key_check检查悬挂引用最后再执行CUSTOM_SQL_STATEMENTS。设置持久 PRAGMA单次设置foreign_keys ON与synchronous NORMAL。better-sqlite3 在整个数据库生命周期内保持单一连接因此这里设置的 PRAGMA 会持续生效无需在每次测试前重放。注入全局 DbService mock调用MockMainDbServiceUtils.setDb(db)与setIsReady(true)将真实数据库挂到全局 mock 的DbService单例上。任何调用application.get(DbService).getDb()的生产代码都会透明地命中测试库。健全性断言校验PRAGMA integrity_check返回ok且foreign_keys为1否则抛错使初始化失败fail loudly而不是让后续测试在损坏状态下静默运行。在每个测试前beforeEachharness 会调用truncateAll(db, sqlite)清空所有用户表数据同时保留表结构与__drizzle_migrations迁移日志。FTS5 影子表shadow table的清理则依靠基表的AFTER DELETE触发器级联完成。truncateAll的实现细节在 tests/helpers/db/internal/truncate.ts它先PRAGMA foreign_keys OFF从sqlite_master中筛选出非sqlite_%、非__drizzle%、非%_fts、非%_fts_%前缀的用户表逐一DELETE在一个事务中顺带清空sqlite_sequence若存在 AUTOINCREMENT 列最后在finally中恢复外键约束。整个文件跑完后afterAll关闭客户端连接、删除临时目录best-effort失败由操作系统回收 tmpdir并调用MockMainDbServiceUtils.resetMocks()复位所有 mock 状态。一个值得注意的细节harness 通过模块级计数器activeHarnessCount检测嵌套调用一旦在同一个 describe 树中重复调用会直接抛错——两个调用会互相覆盖MockMainDbServiceUtils.setDb()导致外层作用域在内层afterAll之后指向过期的数据库。句柄对象db 与 sqlite 双通道setupTestDatabase()返回的TestDatabaseHandle提供两个只读属性见 testDatabase.tsdb: DbType—— Drizzle 数据库实例与生产DbService.getDb()返回类型一致是绝大多数断言的入口sqlite: Database.Database—— 同一个库上的原生 better-sqlite3 连接是逃生通道用于执行 Drizzle 难以表达的原生 SQL/PRAGMA例如sqlite.prepare(...).all()、sqlite.pragma(foreign_key_check)。两个属性都是惰性 getter如果在beforeAll之前访问会抛出明确错误提示应在describe内调用、在it/beforeEach中访问。何时使用、何时不要用应当使用 harness 的场景触及 SQLite 的 Service 测试MessageService、AssistantService等。真实数据库至关重要的 Handler 集成测试例如temporaryChats.integration.test.ts。Seeder 测试。任何需要验证外键级联、FTS5、RETURNING语义或事务行为的用例——恰恰是这些场景下 Drizzle 链式 mock 会失真。不要使用 harness 的场景纯逻辑测试mapper、transformer、Zod schema、分页辅助函数等不涉及数据库。仅验证路由/接线形状的 Handler 测试这类测试合法地 mock 下游 service因为断言目标是调用形状而非数据库状态。src/main/data/migration/v2/migrators/__tests__/*下的 migrator 测试其 mock 上下文经过刻意建模用于验证 migrator 的编排逻辑阶段顺序、幂等性、源回退真实数据库不会在 mock 已覆盖的断言上增加新价值。编排层 Service 测试KnowledgeService、McpService等 mock 了下游数据服务它们验证的是协调逻辑而非持久化。选项seedersTestDatabaseOptions目前只有一个可选字段export interface TestDatabaseOptions { seeders?: ISeeder[] }seeders在 schema 初始化完成后立即执行适用于少数依赖种子数据的 Service 测试如ProviderRegistryService、preset 感知的流程。示例import { PresetProviderSeeder } from data/db/seeding/seeders/presetProviderSeeder setupTestDatabase({ seeders: [new PresetProviderSeeder()] })实现上harness 通过new SeedRunner(db).runAll(options.seeders)执行种子数据见 testDatabase.ts与生产的种子执行路径保持一致。迁移食谱从旧式手写 setup 到统一 harness移除遗留的vi.mock(application, ...)覆盖v2 重构前常见的写法是手搓一个模块级realDb变量并 mockapplication再在beforeEach中手工createClient({ url: file::memory: })initializeTables。现在应替换为 harness- let realDb: DbType | null null - - vi.mock(application, () ({ - application: { - get: vi.fn(() ({ - getDb: vi.fn(() realDb) - })) - } - })) - - const { MessageService } await import(../MessageService) - - describe(MessageService, () { - beforeEach(async () { - const client createClient({ url: file::memory: }) - realDb drizzle({ client, casing: snake_case }) - await initializeTables(realDb) - }) - afterEach(() { realDb null }) - }) import { setupTestDatabase } from test-helpers/db import { messageService } from data/services/MessageService describe(MessageService, () { const dbh setupTestDatabase() // no manual setup — dbh.db is ready in every it() })用状态断言替换 mock 链断言旧风格测试经常构造一长串 mock 链去断言某方法以某参数被调用这既脆弱又看不见数据库真实反应。新风格改为直接执行服务再查询数据库- const values vi.fn().mockReturnValue({ returning: vi.fn().mockResolvedValue([row]) }) - mockInsert.mockReturnValue({ values }) - - await service.create(dto) - - expect(values).toHaveBeenCalledWith({ - name: New Base, - embeddingModelId: embed-model, - ... - }) const created await service.create(dto) expect(created.name).toBe(New Base) const [row] dbh.db.select().from(knowledgeBaseTable) expect(row.name).toBe(New Base) expect(row.embeddingModelId).toBe(embed-model)新形式更强它能捕获 mock 完全看不见的数据库侧约束改写——snake_case 列名映射、NOT NULL 默认值、CHECK 约束拒绝等。只要生产 schema 演进这些约束就真实作用于测试数据。反模式清单使用 harness 时必须避免以下五种写法不要 mockapplication来覆盖DbService全局 setup 已通过mockApplicationFactory()mock 了applicationharness 通过MockMainDbServiceUtils.setDb()注入真实库。测试局部的覆盖会破坏这条注入链路。不要手写CREATE TABLESQLharness 运行的是真实迁移。手写 schema 会在生产 schema 演进时静默漂移真实迁移则会在漂移时响亮地失败。不要在 harness 作用域内使用describe.concurrent/test.concurrentMockMainDbServiceUtils.setDb()是每个测试文件级别的模块单例并发兄弟测试会在该单例与beforeEach截断周期上竞争。不要嵌套调用setupTestDatabase()harness 会对嵌套调用抛出明确错误。把单个调用放在最外层需要数据库的 describe 顶部或将嵌套 describe 拆成兄弟 describe。不要重新添加vi.mock(node:fs, importOriginal)全局 tests/main.setup.ts 已让node:fs、node:os、node:path保持真实实现os.homedir()仍被 stub 为/mock/home。如果确实需要 stub 特定 fs 方法如固定fs.existsSync返回值用vi.spyOn(fs, existsSync)或在测试文件内声明局部vi.mock(node:fs, ...)并借助test-helpers/mocks/nodeFsMock的createNodeFsMock辅助函数。Gotchas三个最容易踩的坑better-sqlite3 原生模块 ABIbetter-sqlite3 是原生模块且不是 N-API——这意味着它是 ABI 相关的必须为加载它的运行时单独编译。一个原生.node只有一个构建槽/一个 ABI而应用Electron与测试系统 Node需要的 ABI 不同。仓库的策略对应 package.json 的 scripts测试运行在Node ABIpnpm install产出的就是 Node ABIVitest运行在系统 Node 下需要它。test:main通过pretest:main钩子先执行pnpm rebuild:nodetest通过pretest钩子执行同样的命令确保套件运行前一定是 Node ABI。Electron 应用入口脚本dev、dev:watch、debug、start与打包需要Electron ABI每个入口脚本都会前置执行pnpm rebuild:electronelectron-rebuild --force --only better-sqlite3配合--force。因此在应用模式与数据库测试之间切换时pretest/pretest:main与应用入口脚本会自动翻转 ABI。如果你在pnpm dev之后立即使用交互式运行器pnpm test:watch、pnpm test:coverage、裸vitest或 IDE 的 Vitest请先手动pnpm rebuild:node或完整跑一次pnpm test:main切回 Node ABI——这些命令并非全部带有pre*钩子。CI 在系统 Node 下安装与测试同样使用 Node ABI。FTS5 与 NULL 内容searchable_text由AFTER INSERT触发器从消息的data.parts含文本的 part填充没有文本 part 的消息会得到空字符串searchable_text触发器用COALESCE(…, )包裹group_concat。FTS5 的AFTER DELETE触发器随后用该值删除索引。这在截断场景下是安全的truncate 可通过但你的 FTS 断言必须考虑空文本这种可能性。Truncate 而非 DropbeforeEach截断用户表不会drop 或重建表。需要物理 drop 表的测试例如损坏回滚的回归测试会破坏该文件中其后所有测试的 harness 状态——这类场景应隔离在专属测试文件中避免共享 harness。底层 Mock 系统速览更完整的 mock 目录说明见 tests/mocks/README.md。harness 依赖的三个关键件test-mocks/main/application——mockApplicationFactory()在 tests/main.setup.ts 中全局接入提供类型安全的application.get()服务访问。test-mocks/main/DbService—— 全局 mock 的MockMainDbServiceUtils定义于 tests/mocks/main/DbService.ts正是 harness 用来把生产查找路由到真实库的载体。该文件同时提供链式查询构建器 mock.run()/.all()/.get()终端同步形态与 better-sqlite3 drizzle 方言对齐、withWriteTx写事务 mock挂接真实连接时委托给.transaction()否则退化为普通 db stub以及快照/checkpoint 相关 no-op spies。test-helpers/mocks/nodeFsMock—— 需要在本地 stubnode:fs的测试的工厂函数全局 setup 已不再 mock fs。配套工具消息树辅助与 harness 自测messageTree.ts 提供两个针对虚拟根virtual-root消息模型的辅助函数专门用于编写对话型数据测试rootRow(topicId)生成话题的虚拟根哨兵行parentId null、role root、空data使用确定性的vroot-topicIdid 便于断言镜像生产MessageService.createRootMessageTx的行为withRoot(topicId, messages)把parentId null的首条消息重新挂到虚拟根之下让测试可以用自然的第一轮对话写法构造嵌套消息树。harness 自身的正确性由 tests/helpers/db/tests/testDatabase.test.ts 覆盖它依次验证初始化后的foreign_keys 1与integrity_check ok、topic 表初始为空、__drizzle_migrations日志保留、FTS5 虚拟表由CUSTOM_SQL_STATEMENTS创建、测试间数据隔离前一个测试插入的行在下一个测试中被截断、事务提交与事务后外键仍开启、AFTER INSERT触发器写入message_fts、truncateAll经AFTER DELETE触发器级联清空 FTS、无文本消息时 truncate 不抛错以及application.get(DbService).getDb()与dbh.db指向同一实例。这套自测本身即是使用 harness 的完整范例遇到不确定的写法时可以直接对照阅读。小结setupTestDatabase()把真实迁移 真实 SQLite 全局服务路由三件事压缩进一行调用让数据库相关测试从mock 链脆弱断言升级为真实状态断言。使用时记住四条主线继承生产迁移不手写 schema、通过MockMainDbServiceUtils.setDb()路由生产代码不局部 mockapplication、每个文件一个 harness不嵌套、不并发、跑测试前确认 Node ABIpnpm rebuild:node。遵循这些约定你写出的测试将同时具备真实性与可维护性。【免费下载链接】cherry-studioAI productivity studio with smart chat, autonomous agents, and 300 assistants. Unified access to frontier LLMs项目地址: https://gitcode.com/GitHub_Trending/ch/cherry-studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表