Cherry Studio 测试 Mock 体系解析:基于tests/__mocks__/的统一测试桩设计与实践
【免费下载链接】cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio
本篇技术指南围绕 Cherry Studio 开源仓库中的统一测试 Mock 目录 tests/mocks/README.md 展开,系统讲解该项目如何按进程类型(Renderer / Main)组织 Mock、如何通过 setup 文件全局注入、以及如何利用@test-mocks路径别名在测试中完成数据播种、错误模拟与生命周期服务桩替。读完本文,你将掌握 Cherry Studio 测试体系的核心约定,并能在自己的测试文件中复用这套 Mock API 编写真实、可维护的单元测试。
一、为什么需要一套“统一”的测试 Mock
Cherry Studio 是典型的 Electron 桌面应用,横跨主进程(Main)、渲染进程(Renderer)与共享层(Shared)。直接对真实服务编写单元测试会遇到三类障碍:
- 环境缺失:jsdom 中没有 Electron 主进程的
ipcMain、BrowserWindow,渲染层也拿不到window.electron桥接对象; - 副作用难以控制:
CacheService、DataApiService、PreferenceService这类横切基础设施牵动数据库、网络与跨窗口通信,测试必须能精准控制其状态; - 生命周期复杂:主进程服务通过
ServiceContainer注册、依赖注入,且带onInit/onStart/onStop/onDestroy生命周期,直接实例化成本高。
因此仓库在 tests/mocks/ 下维护了一套“统一 Mock”,按进程类型分目录组织,并在测试 setup 文件中全局配置,让绝大多数测试无需关心基础设施的桩替细节。
可用 Mock 一览
| 进程 | Mock | 说明 |
|---|---|---|
| Renderer | CacheService | 三层缓存(memory/shared/persist) |
| Renderer | DataApiService | Data API 的 HTTP 客户端 |
| Renderer | PreferenceService | 用户偏好设置 |
| Renderer | useDataApi | Data API Hooks(useQuery、useMutation 等) |
| Renderer | usePreference | 偏好 Hooks |
| Renderer | useCache | 缓存 Hooks |
| Main | application | 统一的应用 Mock 工厂,提供application.get() |
| Main | DbService | 携带 mock db 的数据库服务 |
| Main | CacheService | 内部缓存 + 跨窗口共享缓存 |
| Main | DataApiService | API 协调器 |
| Main | PreferenceService | 偏好服务 |
文件结构
tests/__mocks__/ ├── renderer/ │ ├── CacheService.ts │ ├── DataApiService.ts │ ├── PreferenceService.ts │ ├── useDataApi.ts │ ├── usePreference.ts │ └── useCache.ts ├── main/ │ ├── application.ts │ ├── CacheService.ts │ ├── DataApiService.ts │ ├── DbService.ts │ ├── FileManager.ts # 应用工厂默认实例之一(README 树未列出,见 application.ts) │ └── PreferenceService.ts ├── RendererLoggerService.ts └── MainLoggerService.ts说明:实际目录中 main/application.ts 的
defaultServiceInstances还注册了FileManager、MainWindowService、WindowManager、IpcApiService、JobManager等最小桩替,README 的文件树仅展示了核心基础设施,实际可application.get()的服务比表格更广。
二、测试 Setup:Mock 如何全局生效
Mock 在两个 setup 文件中完成全局注入,分别对应 vitest.config.ts 中main与renderer两个测试项目(项目均声明各自的setupFiles)。
Renderer:tests/renderer.setup.ts
renderer.setup.ts 通过vi.mock把渲染进程关键模块整体替换:
@data/PreferenceService→MockPreferenceService@data/DataApiService→MockDataApiService@data/CacheService→MockCacheService@data/hooks/useDataApi→MockUseDataApi@data/hooks/usePreference→MockUsePreference@data/hooks/useCache→MockUseCache
此外还处理了 jsdom 环境的典型缺口:ResizeObserver、electron.ipcRenderer、window.api桥、uuid、axios、@cherrystudio/ui组件库以及 toast/popup 通知面,beforeEach中统一执行resetToastMocks()/resetPopupMocks()防止共享状态泄漏。同时注意到 setup 对@logger做了全局替换(RendererLoggerService),且对 i18n 采用“懒初始化 + 容错”策略(renderer.setup.ts):只有需要真实翻译的组件测试才会初始化@renderer/i18n/resolver,被 mock 掉 i18n 的文件不会因初始化失败而报错。
Main:tests/main.setup.ts
main.setup.ts 注入的 Mock 更多,涵盖:
- 四个基础设施服务:
@main/data/PreferenceService、@main/data/DataApiService、@main/data/CacheService、@main/data/db/DbService(每个都同时导出类与单例,保持向后兼容,类导出被替换为vi.fn()供serviceRegistry使用); @application整体替换为mockApplicationFactory()产物;- Electron 主进程模块全集:
app、ipcMain、BrowserWindow、dialog、shell、session、webContents、nativeTheme等; - 日志依赖:
winston、winston-daily-rotate-file; electron-store与node:os(homedir固定为/mock/home)。
值得注意的设计:node:fs、node:path、node:os采用...await vi.importActual(...)透传真实实现(main.setup.ts),因为drizzle-orm/better-sqlite3/migrator需要真实读取磁盘上的迁移文件;需要受控行为的测试再通过vi.spyOn(fs, 'existsSync')局部覆盖。
三、Import 路径别名:@test-mocks/*
Mock 工具函数通过别名@test-mocks/*导入,别名指向tests/__mocks__(定义于 electron.vite.config.ts),在 vitest.config.ts 中随mainConfig.resolve.alias/rendererConfig.resolve.alias继承给测试项目。
import { MockCacheUtils } from '@test-mocks/renderer/CacheService' import { MockMainCacheServiceUtils } from '@test-mocks/main/CacheService'注意区分两类导入:
- 被测模块使用生产别名,如
@data/CacheService、@data/hooks/useDataApi——它们已被 setup 全局替换为 Mock; - Mock 工具使用
@test-mocks/*别名——用于在测试体内播种状态、模拟错误、断言调用。
四、Renderer 进程 Mock 详解
4.1 CacheService:三层缓存 Mock
Mock 完整复刻生产CacheService的三层结构(对应 src/renderer/data/CacheService.ts),并内置 TTL 过期逻辑(expireAt检查)与订阅者通知。
方法签名速查:
| 类别 | 方法 | 签名 |
|---|---|---|
| Memory(类型安全) | get/set/has/delete/hasTTL | <K>(key: K) => ...,set带ttl? |
| Memory(casual 动态 key) | getCasual/setCasual/hasCasual/deleteCasual/hasTTLCasual | <T>(key: string) => ... |
| Shared(类型安全) | getShared/getSharedSnapshot/setShared/hasShared/deleteShared/hasSharedTTL | <K>(key: K) => ... |
| Persist | getPersist/setPersist/hasPersist | <K>(key: K) => RendererPersistCacheSchema[K] |
| Hook 管理 | registerHook/unregisterHook | (key: string) => void |
| Ready 状态 | isSharedCacheReady/onSharedCacheReady | — |
| 生命周期 | subscribe/cleanup | — |
从源码(tests/mocks/renderer/CacheService.ts)可以看到若干值得测试利用的“真实感”细节:
- TTL 会真实过期:
get/has命中已过期条目会删除并通知订阅者再返回undefined; - Shared 键有 schema 默认值回退:
getShared对固定 schema 键在未命中时回退到DefaultSharedCache,而模板实例在运行时 miss 返回undefined;getSharedSnapshot则是纯粹的物理读,不做 TTL/淘汰/默认值处理,属于内部 API,生产环境只允许useCache.ts使用,测试可自由探测; delete会拒绝删除被 Hook 占用的键:当activeHookCounts中存在该键时返回false并打印错误,模拟生产中的 Hook 引用保护;- Persist 写操作去重:
setPersist使用isEqual比较新旧值,相同则静默返回,保证通知次数与真实服务一致。
使用示例
import { cacheService } from '@data/CacheService' import { MockCacheUtils } from '@test-mocks/renderer/CacheService' describe('Cache', () => { beforeEach(() => MockCacheUtils.resetMocks()) it('basic usage', () => { cacheService.setCasual('key', { data: 'value' }, 5000) expect(cacheService.getCasual('key')).toEqual({ data: 'value' }) }) it('with test utilities', () => { MockCacheUtils.setInitialState({ memory: [['key', 'value']], shared: [['shared.key', 'shared']], persist: [['persist.key', 'persist']] }) }) })MockCacheUtils还提供triggerCacheChange(模拟订阅事件)、setSharedCacheReady(切换 shared 缓存就绪态并触发回调)、simulateTTLExpiration(把条目过期时间改为过去,验证过期分支)、getCurrentState(检查内部三层状态与 Hook 计数)。
4.2 DataApiService:HTTP 客户端 Mock
提供与生产一致的get/post/put/patch/delete,以及数据变更通知与重试配置:
| 方法 | 签名 |
|---|---|
get | (path, options?) => Promise<any> |
post | (path, options) => Promise<any> |
put | (path, options) => Promise<any> |
patch | (path, options) => Promise<any> |
delete | (path, options?) => Promise<any> |
onDataChanged | (endpoints, listener) => () => void— 函数式扇出,生产语义 |
configureRetry | (options) => void |
getRetryConfig | () => RetryOptions |
import { dataApiService } from '@data/DataApiService' import { MockDataApiUtils } from '@test-mocks/renderer/DataApiService' describe('API', () => { beforeEach(() => MockDataApiUtils.resetMocks()) it('basic request', async () => { const response = await dataApiService.get('/topics') expect(response.topics).toBeDefined() }) it('custom response', async () => { MockDataApiUtils.setCustomResponse('/topics', 'GET', { custom: true }) const response = await dataApiService.get('/topics') expect(response.custom).toBe(true) }) it('error simulation', async () => { MockDataApiUtils.setErrorResponse('/topics', 'GET', new Error('Failed')) await expect(dataApiService.get('/topics')).rejects.toThrow('Failed') }) it('data change convergence', () => { const listener = vi.fn() dataApiService.onDataChanged('/topics', listener) MockDataApiUtils.emitDataChange([{ endpoint: '/topics', kind: 'membership' }]) expect(listener).toHaveBeenCalledWith([{ endpoint: '/topics', kind: 'membership' }]) }) })对于 Hook 消费者,useDataApiMock 暴露同样的配对:useDataChange注册监听器,MockUseDataApiUtils.emitDataChange(effects)以生产批量语义投递通知。两者都委托给DataApiServiceMock 的扇出——与生产分层一致,因此通过任一工具发射通知都能到达任一模块注册的监听器(tests/mocks/renderer/useDataApi.ts)。
4.3 useDataApi Hooks
React 数据操作 Hooks 的 Mock 提供与生产一致的签名(真实实现在 src/renderer/data/hooks/useDataApi.ts),并按 API 路径自动生成默认数据。
| Hook | 签名 | 返回 |
|---|---|---|
useQuery | (path, options?) | { data, loading, error, refetch, mutate } |
useMutation | (method, path, options?) | { mutate, loading, error } |
usePaginatedQuery | (path, options?) | { items, total, page, loading, error, hasMore, hasPrev, prevPage, nextPage, refresh, reset } |
useInvalidateCache | () | (keys?) => Promise<any> |
useReadCache | () | (path, query?) => TResponse \| undefined |
useWriteCache | () | async (path, value, query?) => void |
从源码看,实际 Mock 模块还导出了 README 表格之外的能力:useInfiniteQuery(游标分页)、useInfiniteFlatItems(展平pages[].items)、useDataChange、prefetch,测试中同样可以直接使用。
默认数据生成逻辑(tests/mocks/renderer/useDataApi.ts)对常见路径返回结构化的假数据:/topics返回两条带id/name/createdAt的主题列表,/messages返回 user/assistant 两条消息,/paintings返回空游标分页结构;未知路径回退到{ id: 'mock_id', data: 'mock_data' }。
import { useQuery, useMutation, useReadCache, useWriteCache } from '@data/hooks/useDataApi' import { MockUseDataApiUtils } from '@test-mocks/renderer/useDataApi' describe('Hooks', () => { beforeEach(() => MockUseDataApiUtils.resetMocks()) it('useQuery', () => { const { data, loading } = useQuery('/topics') expect(loading).toBe(false) expect(data).toBeDefined() }) it('useMutation', async () => { const { mutate } = useMutation('POST', '/topics') const result = await mutate({ body: { name: 'New' } }) expect(result.created).toBe(true) }) it('custom data', () => { MockUseDataApiUtils.mockQueryData('/topics', { custom: true }) const { data } = useQuery('/topics') expect(data.custom).toBe(true) }) it('useReadCache reads seeded values', () => { // 键形状与生产一致:省略 `query` 得到 [path],传入非空 `query` 得到 [path, query] MockUseDataApiUtils.seedCache('/topics', { topics: [{ id: 't1' }], total: 1 }) const read = useReadCache() expect(read('/topics')).toEqual({ topics: [{ id: 't1' }], total: 1 }) }) it('useWriteCache persists to mock store (assertable via getCachedValue)', async () => { const write = useWriteCache() await write('/topics', { topics: [], total: 0 }) expect(MockUseDataApiUtils.getCachedValue('/topics')).toEqual({ topics: [], total: 0 }) }) })注意:
useReadCache/useWriteCache底层共享同一个内存Map。resetMocks()会同时清空调用历史与缓存存储;如果只想清缓存而保留 Hook Mock,请使用clearCache()。
MockUseDataApiUtils还提供丰富的场景模拟:mockQueryLoading/mockQueryResult/mockQueryError、mockMutationSuccess/mockMutationError/mockMutationLoading/mockMutationWithTrigger(允许传入自定义vi.fn()断言调用参数)、mockPaginatedData等。
4.4 useCache Hooks
缓存操作 Hooks:
| Hook | 签名 | 返回 |
|---|---|---|
useCache | (key, initValue?) | [value, setValue] |
useSharedCache | (key, initValue?) | [value, setValue] |
useSharedCacheValue | (key) | value \| undefined(只读,绝不物化默认值) |
useSharedCacheSelector | (keys, selector, isEqual?) | selector(values),基于当前 mock 的 shared 值(只读,逐渲染求值、非响应式) |
usePersistCache | (key) | [value, setValue] |
setValue接受具体值或函数式更新器(prev) => next(镜像生产实现)。Mock 会基于最新 mock 值解析更新器,并带同样的默认值回退,因此使用函数式更新的调用点无需改动即可在 Mock 下运行。
import { useCache } from '@data/hooks/useCache' const [value, setValue] = useCache('key', 'default') setValue('new value') setValue((prev) => prev + '!') // 函数式更新器4.5 usePreference Hooks
偏好设置 Hooks(真实实现见 src/renderer/data/hooks/usePreference.ts):
| Hook | 签名 | 返回 |
|---|---|---|
usePreference | (key) | [value, setValue] |
useMultiplePreferences | (keyMap) | [values, setValues] |
import { usePreference } from '@data/hooks/usePreference' const [theme, setTheme] = usePreference('ui.theme') await setTheme('dark')五、Main 进程 Mock 详解
5.1 范围约定:只 Mock 横切基础设施
tests/mocks/main/ 只承载横切基础设施的 Mock:PreferenceService、CacheService、DbService、DataApiService,外加最小化的MainWindowService/WindowManager桩替,全部经 main.setup.ts 全局预置。
不要为特性化的生命周期服务(如FileProcessingTaskService、KnowledgeRuntimeService)在此添加文件。ServiceOverrides类型被刻意锁定为keyof typeof defaultServiceInstances(tests/mocks/main/application.ts)以强制执行该边界。特性服务应在测试文件内局部桩替(见 5.4 节)。
| 服务类别 | 如何 Mock |
|---|---|
| 基础设施(上表所列) | 已全局 Mock;通过mockApplicationFactory({ Name: {...} })覆盖 |
| 特性化生命周期服务 | 测试文件内局部vi.mock('@application')+MockBaseService |
| 直接导入的单例(无生命周期) | 直接vi.mock('path/to/module') |
5.2 Application Mock:统一工厂
所有主进程测试都通过 main.setup.ts 全局获得application.get()的 Mock。需要自定义服务实例的测试可通过mockApplicationFactory(overrides)覆盖特定服务。
API
| 导出 | 说明 |
|---|---|
mockApplicationFactory(overrides?) | 返回完整 Mock 模块{ application, serviceList },供vi.mock()使用 |
createMockApplication(overrides?) | 只返回 Mockapplication对象 |
defaultServiceInstances | 所有已注册服务的默认 Mock 实例 |
从源码可以看到 Mock 容器对生产ServiceContainer语义的精心复刻(tests/mocks/main/application.ts):
get(name)命中返回实例,未知名称抛出Unknown service;getOptional(name)对已注册服务抛出(提示“不是条件服务,请用 get()”),对未知名称返回undefined——这能抓住错误地把普通服务的get()降级成getOptional()的代码;- Mock 还提供
getPath(返回/mock/<key>确定性路径)、isQuitting可变标志(测试可置true触发退出感知代码路径)、quit/relaunch/shutdown等间谍方法。
getOptional语义:Mock 无法完全证明容器语义——如果get/getOptional的选择对某段代码至关重要,应补充真实ServiceContainer的冒烟测试(参见 dataApiDataChange.container.test.ts)。
使用方式
全局 setup(main.setup.ts 已配置):
vi.mock('@application', async () => { const { mockApplicationFactory } = await import('./__mocks__/main/application') return mockApplicationFactory() })在单个测试文件中覆盖基础设施服务:
const mockDb = { select: vi.fn(), insert: vi.fn() } vi.mock('@application', async () => { const { mockApplicationFactory } = await import('@test-mocks/main/application') return mockApplicationFactory({ DbService: { getDb: () => mockDb } }) })对于非基础设施服务,不要在这里覆盖——请使用 Testing Other Lifecycle Services 一节的方式在测试文件内局部桩替。
5.3 四个基础设施 Mock
Main DbService
提供对 Mock SQLite 数据库的访问:
| 方法 | 签名 |
|---|---|
getDb | () => MockDb |
withWriteTx | <T>(fn: (tx) => T) => T(同步,镜像生产) |
isReady | boolean(getter) |
import { MockMainDbServiceUtils } from '@test-mocks/main/DbService' beforeEach(() => MockMainDbServiceUtils.resetMocks()) // 使用默认 mock db MockMainDbServiceUtils.getDefaultMockDb() // 替换为自定义 db MockMainDbServiceUtils.setDb(customMockDb)底层默认 db(tests/mocks/main/DbService.ts)是链式、同步(better-sqlite3 形态)的 drizzle 查询构建器:select/insert/update/delete返回同一构建器,链式方法(from/where/set/values/limit/offset/orderBy/groupBy/having/returning/onConflictDoUpdate/leftJoin等)均可继续链式调用;同步终结器.run()返回{ changes: 0, lastInsertRowid: 0 },.all()返回[],.get()返回undefined。
withWriteTx的双模式行为:一旦真实数据库被挂载(setupTestDatabase()→setDb()),withWriteTx会委托给db.transaction(fn, { behavior: 'immediate' })以获得真实事务语义(抛错回滚等);纯 Mock 模式(默认 db、未setDb())则退化为fn(this.db)——无互斥锁、无 BUSY 重试。需要自定义行为时用vi.spyOn(dbServiceInstance, 'withWriteTx')注入。手写 DbService Mock 必须包含此方法,否则生产代码会抛TypeError: dbService.withWriteTx is not a function。
Main CacheService
内部缓存 + 跨窗口共享缓存:
| 类别 | 方法 | 签名 |
|---|---|---|
| 生命周期 | initialize/cleanup | () => Promise<void>/() => void |
| 内部缓存 | get/set/has/delete | <T>(key: string, ...) |
| 共享缓存 | getShared/setShared/hasShared/deleteShared | <K>(key: K, ...) |
| 订阅 | subscribeChange/subscribeSharedChange | <T>(key, callback) => () => void |
import { MockMainCacheServiceUtils } from '@test-mocks/main/CacheService' beforeEach(() => MockMainCacheServiceUtils.resetMocks()) MockMainCacheServiceUtils.setCacheValue('key', 'value') MockMainCacheServiceUtils.setSharedCacheValue('shared.key', 'shared')订阅 Mock 的局限:
subscribeChange/subscribeSharedChange是调用追踪桩,不复现真实的触发语义。它们的用途是验证registerDisposable(cacheService.subscribeChange(...))接线与订阅确实发生,而不是模拟回调触发。另外setShared/deleteShared会无条件把每次调用记入broadcastCalls(不做isEqual短路),以保持getBroadcastHistory()消费方的向后兼容。
Main DataApiService
管理 ApiServer 与 IpcAdapter 的 API 协调器:
| 方法 | 签名 |
|---|---|
initialize | () => Promise<void> |
shutdown | () => Promise<void> |
getSystemStatus | () => object |
getApiServer | () => ApiServer |
import { MockMainDataApiServiceUtils } from '@test-mocks/main/DataApiService' beforeEach(() => MockMainDataApiServiceUtils.resetMocks()) MockMainDataApiServiceUtils.simulateInitializationError(new Error('Failed'))Main PreferenceService
带类型化键的偏好存储,以DefaultPreferences.default为种子:
| 方法 | 签名 |
|---|---|
initialize | () => Promise<void> |
get | <K>(key: K) => UnifiedPreferenceType[K] |
set | <K>(key: K, value) => Promise<void> |
getMultiple | <K>(keys: K[]) => Record<K, UnifiedPreferenceType[K]> |
setMultiple | (values) => Promise<void> |
subscribeForWindow | (windowId, keys) => void |
import { MockMainPreferenceServiceUtils } from '@test-mocks/main/PreferenceService' beforeEach(() => MockMainPreferenceServiceUtils.resetMocks()) // 播种偏好值 MockMainPreferenceServiceUtils.setPreferenceValue('ui.theme', 'dark') // 模拟外部变更(触发主进程订阅者) MockMainPreferenceServiceUtils.simulateExternalPreferenceChange('ui.theme', 'light')工具函数还包括:getPreferenceValue、setMultiplePreferenceValues、getAllPreferenceValues、simulateWindowSubscription、getSubscriptionCounts。
5.4 测试其他生命周期服务(局部桩替)
特性化的生命周期服务应当在测试文件内局部桩替。一个典型测试需要三处替换:@application、BaseService与生命周期装饰器。
规范配置
import type * as LifecycleModule from '@main/core/lifecycle' import { getDependencies, getPhase } from '@main/core/lifecycle/decorators' import { Phase } from '@main/core/lifecycle/types' import { beforeEach, describe, expect, it, vi } from 'vitest' const { appGetMock, startTaskMock, getTaskMock } = vi.hoisted(() => ({ appGetMock: vi.fn(), startTaskMock: vi.fn(), getTaskMock: vi.fn() })) vi.mock('@application', () => ({ application: { get: appGetMock } })) vi.mock('@main/core/lifecycle', async (importOriginal) => { const actual = await importOriginal<typeof LifecycleModule>() class MockBaseService { ipcHandle = vi.fn() protected readonly _disposables: Array<{ dispose: () => void } | (() => void)> = [] protected registerDisposable<T extends { dispose: () => void } | (() => void)>(d: T): T { this._disposables.push(d) return d } } return { ...actual, BaseService: MockBaseService } }) beforeEach(() => { vi.clearAllMocks() appGetMock.mockImplementation((name: string) => { if (name === 'FileProcessingTaskService') { return { startTask: startTaskMock, getTask: getTaskMock } } throw new Error(`Unexpected application.get(${name})`) }) }) // 声明 Mock 之后再导入被测模块(SUT) const { FileProcessingService } = await import('../FileProcessingService')关键点:用vi.hoisted提升间谍、在beforeEach中给application.get注入按名称分发的实现(未预期的名称直接抛错,保证依赖声明完备)、并通过importOriginal保留生命周期模块的getPhase/getDependencies等真实装饰器。
常用断言技巧
测试容器不运行,因此要手动驱动onInit/onStart/onStop/onDestroy生命周期钩子:
| 目标 | 方式 |
|---|---|
| Phase | expect(getPhase(MyService)).toBe(Phase.WhenReady) |
| 依赖 | expect(getDependencies(MyService)).toEqual(['OtherService']) |
| 注册的 IPC 通道 | const svc = new MyService(); (svc as any).onInit(); (svc as any).ipcHandle.mock.calls.map(c => c[0]) |
| 单个 IPC 处理器 | ipcHandle.mock.calls.find(c => c[0] === 'channel')?.[1],然后调用 |
| Disposables | 驱动生命周期后检查(svc as any)._disposables |
参考实现
- KnowledgeService.test.ts —— dispatch 桩 + phase/deps + 逐通道处理器检查(注意 README 写的是
src/main/services/knowledge/...,当前仓库实际位于src/main/features/knowledge/__tests__/) - ShortcutService.test.ts —— 更丰富的
MockBaseService(含registerDisposable+ no-op 装饰器替换)
六、Best Practices 与 Troubleshooting
实践约定
- 基础设施服务已全局预置 Mock,通过
mockApplicationFactory({ Name: {...} })覆盖,而不是临时手写application.getMock; - 特性化生命周期服务在测试文件内局部桩替——不要把它们加进
tests/__mocks__/main/或defaultServiceInstances; - 每个基础设施 Mock 都暴露
MockMain<Name>ServiceUtils,含resetMocks()及服务专属助手(播种值、模拟错误)。在beforeEach中调用resetMocks()。
常见问题排查
| 问题 | 解决方案 |
|---|---|
| Mock 未生效 | 检查测试是否运行在正确的进程项目(renderer/main,见 vitest.config.ts) |
| 类型错误 | 确保 Mock 与真实接口匹配,必要时使用类型断言 |
| 状态污染 | 在beforeEach中调用resetMocks() |
| 导入问题 | 使用路径别名(@data/CacheService)而非相对路径 |
七、小结
Cherry Studio 的 tests/mocks/ 展示了 Electron 应用中“按进程分层 + 全局 setup 注入 + 工具函数播种”的 Mock 组织范本:渲染进程 Mock 覆盖三层缓存、Data API 客户端与三大 Hook 族,并在行为细节上贴近生产(TTL 过期、schema 默认值、函数式更新器、批量变更通知);主进程 Mock 通过统一的应用工厂复刻ServiceContainer语义,严格区分“横切基础设施”与“特性化服务”的桩替边界,并提供了可复制的生命周期服务测试模板。这套体系让开发者可以在完全可控、确定性的环境下验证从缓存读写到 IPC 通道注册的每一层行为。
【免费下载链接】cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考