Cherry Studio 测试 Mock 体系解析:基于 `tests/__mocks__/` 的统一测试桩设计与实践
2026/9/19 5:38:09 网站建设 项目流程

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)。直接对真实服务编写单元测试会遇到三类障碍:

  1. 环境缺失:jsdom 中没有 Electron 主进程的ipcMainBrowserWindow,渲染层也拿不到window.electron桥接对象;
  2. 副作用难以控制CacheServiceDataApiServicePreferenceService这类横切基础设施牵动数据库、网络与跨窗口通信,测试必须能精准控制其状态;
  3. 生命周期复杂:主进程服务通过ServiceContainer注册、依赖注入,且带onInit/onStart/onStop/onDestroy生命周期,直接实例化成本高。

因此仓库在 tests/mocks/ 下维护了一套“统一 Mock”,按进程类型分目录组织,并在测试 setup 文件中全局配置,让绝大多数测试无需关心基础设施的桩替细节。

可用 Mock 一览

进程Mock说明
RendererCacheService三层缓存(memory/shared/persist)
RendererDataApiServiceData API 的 HTTP 客户端
RendererPreferenceService用户偏好设置
RendereruseDataApiData API Hooks(useQuery、useMutation 等)
RendererusePreference偏好 Hooks
RendereruseCache缓存 Hooks
Mainapplication统一的应用 Mock 工厂,提供application.get()
MainDbService携带 mock db 的数据库服务
MainCacheService内部缓存 + 跨窗口共享缓存
MainDataApiServiceAPI 协调器
MainPreferenceService偏好服务

文件结构

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还注册了FileManagerMainWindowServiceWindowManagerIpcApiServiceJobManager等最小桩替,README 的文件树仅展示了核心基础设施,实际可application.get()的服务比表格更广。

二、测试 Setup:Mock 如何全局生效

Mock 在两个 setup 文件中完成全局注入,分别对应 vitest.config.ts 中mainrenderer两个测试项目(项目均声明各自的setupFiles)。

Renderer:tests/renderer.setup.ts

renderer.setup.ts 通过vi.mock把渲染进程关键模块整体替换:

  • @data/PreferenceServiceMockPreferenceService
  • @data/DataApiServiceMockDataApiService
  • @data/CacheServiceMockCacheService
  • @data/hooks/useDataApiMockUseDataApi
  • @data/hooks/usePreferenceMockUsePreference
  • @data/hooks/useCacheMockUseCache

此外还处理了 jsdom 环境的典型缺口:ResizeObserverelectron.ipcRendererwindow.api桥、uuidaxios@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 主进程模块全集:appipcMainBrowserWindowdialogshellsessionwebContentsnativeTheme等;
  • 日志依赖:winstonwinston-daily-rotate-file
  • electron-storenode:oshomedir固定为/mock/home)。

值得注意的设计:node:fsnode:pathnode: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) => ...setttl?
Memory(casual 动态 key)getCasual/setCasual/hasCasual/deleteCasual/hasTTLCasual<T>(key: string) => ...
Shared(类型安全)getShared/getSharedSnapshot/setShared/hasShared/deleteShared/hasSharedTTL<K>(key: K) => ...
PersistgetPersist/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 返回undefinedgetSharedSnapshot则是纯粹的物理读,不做 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)、useDataChangeprefetch,测试中同样可以直接使用。

默认数据生成逻辑(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底层共享同一个内存MapresetMocks()会同时清空调用历史与缓存存储;如果只想清缓存而保留 Hook Mock,请使用clearCache()

MockUseDataApiUtils还提供丰富的场景模拟:mockQueryLoading/mockQueryResult/mockQueryErrormockMutationSuccess/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:PreferenceServiceCacheServiceDbServiceDataApiService,外加最小化的MainWindowService/WindowManager桩替,全部经 main.setup.ts 全局预置。

不要为特性化的生命周期服务(如FileProcessingTaskServiceKnowledgeRuntimeService)在此添加文件。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(同步,镜像生产)
isReadyboolean(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')

工具函数还包括:getPreferenceValuesetMultiplePreferenceValuesgetAllPreferenceValuessimulateWindowSubscriptiongetSubscriptionCounts

5.4 测试其他生命周期服务(局部桩替)

特性化的生命周期服务应当在测试文件内局部桩替。一个典型测试需要三处替换:@applicationBaseService与生命周期装饰器。

规范配置
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生命周期钩子:

目标方式
Phaseexpect(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

实践约定

  1. 基础设施服务已全局预置 Mock,通过mockApplicationFactory({ Name: {...} })覆盖,而不是临时手写application.getMock;
  2. 特性化生命周期服务在测试文件内局部桩替——不要把它们加进tests/__mocks__/main/defaultServiceInstances
  3. 每个基础设施 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询