cherry-studio/tests/__mocks__
fullex d397a43806 fix: typecheck/test:lint/format:check
feat: add data related test
2025-09-16 15:26:36 +08:00
..
main fix: typecheck/test:lint/format:check 2025-09-16 15:26:36 +08:00
renderer fix: typecheck/test:lint/format:check 2025-09-16 15:26:36 +08:00
MainLoggerService.ts feat: use oxlint to speed up lint (#10168) 2025-09-15 19:42:13 +08:00
README.md test: update tests to use usePreference hook and improve snapshot consistency 2025-09-16 14:07:54 +08:00
RendererLoggerService.ts feat: use oxlint to speed up lint (#10168) 2025-09-15 19:42:13 +08:00

Test Mocks

这个目录包含了项目中使用的统一测试模拟mocks。这些模拟按照进程类型组织避免重名冲突并在相应的测试设置文件中全局配置。

🎯 统一模拟概述

已实现的统一模拟

Renderer Process Mocks

  • PreferenceService - 渲染进程偏好设置服务模拟
  • DataApiService - 渲染进程数据API服务模拟
  • CacheService - 渲染进程三层缓存服务模拟
  • useDataApi hooks - 数据API钩子模拟 (useQuery, useMutation, usePaginatedQuery, etc.)
  • usePreference hooks - 偏好设置钩子模拟 (usePreference, useMultiplePreferences)
  • useCache hooks - 缓存钩子模拟 (useCache, useSharedCache, usePersistCache)

Main Process Mocks

  • PreferenceService - 主进程偏好设置服务模拟
  • DataApiService - 主进程数据API服务模拟
  • CacheService - 主进程缓存服务模拟

🌟 核心优势

  • 进程分离: 按照renderer/main分开组织避免重名冲突
  • 自动应用: 无需在每个测试文件中单独模拟
  • 完整API覆盖: 实现了所有服务和钩子的完整API
  • 类型安全: 完全支持 TypeScript保持与真实服务的类型兼容性
  • 现实行为: 模拟提供现实的默认值和行为模式
  • 高度可定制: 支持为特定测试定制行为
  • 测试工具: 内置丰富的测试工具函数

📁 文件结构

tests/__mocks__/
├── README.md                    # 本文档
├── renderer/                    # 渲染进程模拟
│   ├── PreferenceService.ts     # 渲染进程偏好设置服务模拟
│   ├── DataApiService.ts        # 渲染进程数据API服务模拟
│   ├── CacheService.ts          # 渲染进程缓存服务模拟
│   ├── useDataApi.ts            # 数据API钩子模拟
│   ├── usePreference.ts         # 偏好设置钩子模拟
│   └── useCache.ts              # 缓存钩子模拟
├── main/                        # 主进程模拟
│   ├── PreferenceService.ts     # 主进程偏好设置服务模拟
│   ├── DataApiService.ts        # 主进程数据API服务模拟
│   └── CacheService.ts          # 主进程缓存服务模拟
├── RendererLoggerService.ts     # 渲染进程日志服务模拟
└── MainLoggerService.ts         # 主进程日志服务模拟

🔧 测试设置

Renderer Process Tests

tests/renderer.setup.ts 中配置了所有渲染进程模拟:

// 自动加载 renderer/ 目录下的模拟
vi.mock('@data/PreferenceService', async () => {
  const { MockPreferenceService } = await import('./__mocks__/renderer/PreferenceService')
  return MockPreferenceService
})
// ... 其他渲染进程模拟

Main Process Tests

tests/main.setup.ts 中配置了所有主进程模拟:

// 自动加载 main/ 目录下的模拟
vi.mock('@main/data/PreferenceService', async () => {
  const { MockMainPreferenceServiceExport } = await import('./__mocks__/main/PreferenceService')
  return MockMainPreferenceServiceExport
})
// ... 其他主进程模拟

PreferenceService Mock

简介

PreferenceService.ts 提供了 PreferenceService 的统一模拟实现,用于所有渲染进程测试。这个模拟:

  • 自动应用:在 renderer.setup.ts 中全局配置,无需在每个测试文件中单独模拟
  • 完整API:实现了 PreferenceService 的所有方法get, getMultiple, set, etc.
  • 合理默认值:提供了常用偏好设置的默认值
  • 可定制:支持为特定测试定制默认值
  • 类型安全:完全支持 TypeScript 类型检查

默认值

模拟提供了以下默认偏好设置:

// 导出偏好设置
'data.export.markdown.force_dollar_math': false
'data.export.markdown.exclude_citations': false
'data.export.markdown.standardize_citations': true
'data.export.markdown.show_model_name': false
'data.export.markdown.show_model_provider': false

// UI偏好设置
'ui.language': 'en'
'ui.theme': 'light'
'ui.font_size': 14

// AI偏好设置
'ai.default_model': 'gpt-4'
'ai.temperature': 0.7
'ai.max_tokens': 2000

// 功能开关
'feature.web_search': true
'feature.reasoning': false
'feature.tool_calling': true

基本使用

由于模拟已经全局配置,大多数测试可以直接使用 PreferenceService无需额外设置

import { preferenceService } from '@data/PreferenceService'

describe('MyComponent', () => {
  it('should use preference values', async () => {
    // PreferenceService 已经被自动模拟
    const value = await preferenceService.get('ui.theme')
    expect(value).toBe('light') // 使用默认值
  })
})

高级使用

1. 修改单个测试的偏好值

import { preferenceService } from '@data/PreferenceService'
import { vi } from 'vitest'

describe('Custom preferences', () => {
  it('should work with custom preference values', async () => {
    // 为这个测试修改特定值
    ;(preferenceService.get as any).mockImplementation((key: string) => {
      if (key === 'ui.theme') return Promise.resolve('dark')
      // 其他键使用默认模拟行为
      return vi.fn().mockResolvedValue(null)()
    })

    const theme = await preferenceService.get('ui.theme')
    expect(theme).toBe('dark')
  })
})

2. 重置模拟状态

import { preferenceService } from '@data/PreferenceService'

describe('Mock state management', () => {
  beforeEach(() => {
    // 重置模拟到初始状态
    if ('_resetMockState' in preferenceService) {
      ;(preferenceService as any)._resetMockState()
    }
  })
})

3. 检查模拟内部状态

import { preferenceService } from '@data/PreferenceService'

describe('Mock inspection', () => {
  it('should allow inspecting mock state', () => {
    // 查看当前模拟状态
    if ('_getMockState' in preferenceService) {
      const state = (preferenceService as any)._getMockState()
      console.log('Current mock state:', state)
    }
  })
})

4. 为整个测试套件定制默认值

如果需要为特定的测试文件定制默认值,可以在该文件中重新模拟:

import { vi } from 'vitest'

// 重写全局模拟,添加自定义默认值
vi.mock('@data/PreferenceService', async () => {
  const { createMockPreferenceService } = await import('tests/__mocks__/PreferenceService')

  // 定制默认值
  const customDefaults = {
    'my.custom.setting': 'custom_value',
    'ui.theme': 'dark' // 覆盖默认值
  }

  return {
    preferenceService: createMockPreferenceService(customDefaults)
  }
})

测试验证

可以验证 PreferenceService 方法是否被正确调用:

import { preferenceService } from '@data/PreferenceService'
import { vi } from 'vitest'

describe('Preference service calls', () => {
  it('should call preference service methods', async () => {
    await preferenceService.get('ui.theme')

    // 验证方法调用
    expect(preferenceService.get).toHaveBeenCalledWith('ui.theme')
    expect(preferenceService.get).toHaveBeenCalledTimes(1)
  })
})

添加新的默认值

当项目中添加新的偏好设置时,请在 PreferenceService.tsmockPreferenceDefaults 中添加相应的默认值:

export const mockPreferenceDefaults: Record<string, any> = {
  // 现有默认值...

  // 新增默认值
  'new.feature.enabled': true,
  'new.feature.config': { option: 'value' }
}

这样可以确保所有测试都能使用合理的默认值,减少测试失败的可能性。

DataApiService Mock

简介

DataApiService.ts 提供了数据API服务的统一模拟支持所有HTTP方法和高级功能。

功能特性

  • 完整HTTP支持: GET, POST, PUT, PATCH, DELETE
  • 批量操作: batch() 和 transaction() 支持
  • 订阅系统: subscribe/unsubscribe 模拟
  • 连接管理: connect/disconnect/ping 方法
  • 智能模拟数据: 基于路径自动生成合理的响应

基本使用

import { dataApiService } from '@data/DataApiService'

describe('API Integration', () => {
  it('should fetch topics', async () => {
    // 自动模拟,返回预设的主题列表
    const response = await dataApiService.get('/api/topics')
    expect(response.success).toBe(true)
    expect(response.data.topics).toHaveLength(2)
  })
})

高级使用

import { MockDataApiUtils } from 'tests/__mocks__/DataApiService'

describe('Custom API behavior', () => {
  beforeEach(() => {
    MockDataApiUtils.resetMocks()
  })

  it('should handle custom responses', async () => {
    // 设置特定路径的自定义响应
    MockDataApiUtils.setCustomResponse('/api/topics', 'GET', {
      topics: [{ id: 'custom', name: 'Custom Topic' }]
    })

    const response = await dataApiService.get('/api/topics')
    expect(response.data.topics[0].name).toBe('Custom Topic')
  })

  it('should simulate errors', async () => {
    // 模拟错误响应
    MockDataApiUtils.setErrorResponse('/api/topics', 'GET', 'Network error')

    const response = await dataApiService.get('/api/topics')
    expect(response.success).toBe(false)
    expect(response.error?.message).toBe('Network error')
  })
})

CacheService Mock

简介

CacheService.ts 提供了三层缓存系统的完整模拟:内存缓存、共享缓存和持久化缓存。

功能特性

  • 三层架构: 内存、共享、持久化缓存
  • 订阅系统: 支持缓存变更订阅
  • TTL支持: 模拟缓存过期(简化版)
  • Hook引用跟踪: 模拟生产环境的引用管理
  • 默认值: 基于缓存schema的智能默认值

基本使用

import { cacheService } from '@data/CacheService'

describe('Cache Operations', () => {
  it('should store and retrieve cache values', () => {
    // 设置缓存值
    cacheService.set('user.preferences', { theme: 'dark' })

    // 获取缓存值
    const preferences = cacheService.get('user.preferences')
    expect(preferences.theme).toBe('dark')
  })

  it('should work with persist cache', () => {
    // 持久化缓存操作
    cacheService.setPersist('app.last_opened_topic', 'topic123')
    const lastTopic = cacheService.getPersist('app.last_opened_topic')
    expect(lastTopic).toBe('topic123')
  })
})

高级测试工具

import { MockCacheUtils } from 'tests/__mocks__/CacheService'

describe('Advanced cache testing', () => {
  beforeEach(() => {
    MockCacheUtils.resetMocks()
  })

  it('should set initial cache state', () => {
    // 设置初始缓存状态
    MockCacheUtils.setInitialState({
      memory: [['theme', 'dark'], ['language', 'en']],
      persist: [['app.version', '1.0.0']]
    })

    expect(cacheService.get('theme')).toBe('dark')
    expect(cacheService.getPersist('app.version')).toBe('1.0.0')
  })

  it('should simulate cache changes', () => {
    let changeCount = 0
    cacheService.subscribe('theme', () => changeCount++)

    MockCacheUtils.triggerCacheChange('theme', 'light')
    expect(changeCount).toBe(1)
  })
})

useDataApi Hooks Mock

简介

useDataApi.ts 提供了所有数据API钩子的统一模拟包括查询、变更和分页功能。

支持的钩子

  • useQuery - 数据查询钩子
  • useMutation - 数据变更钩子
  • usePaginatedQuery - 分页查询钩子
  • useInvalidateCache - 缓存失效钩子
  • prefetch - 预取函数

基本使用

import { useQuery, useMutation } from '@data/hooks/useDataApi'

describe('Data API Hooks', () => {
  it('should work with useQuery', () => {
    const { data, isLoading, error } = useQuery('/api/topics')

    // 默认返回模拟数据
    expect(data).toBeDefined()
    expect(data.topics).toHaveLength(2)
    expect(isLoading).toBe(false)
    expect(error).toBeUndefined()
  })

  it('should work with useMutation', async () => {
    const { trigger, isMutating } = useMutation('/api/topics', 'POST')

    const result = await trigger({ name: 'New Topic' })
    expect(result.created).toBe(true)
    expect(result.name).toBe('New Topic')
  })
})

自定义测试行为

import { MockUseDataApiUtils } from 'tests/__mocks__/useDataApi'

describe('Custom hook behavior', () => {
  beforeEach(() => {
    MockUseDataApiUtils.resetMocks()
  })

  it('should mock loading state', () => {
    MockUseDataApiUtils.mockQueryLoading('/api/topics')

    const { data, isLoading } = useQuery('/api/topics')
    expect(isLoading).toBe(true)
    expect(data).toBeUndefined()
  })

  it('should mock error state', () => {
    const error = new Error('API Error')
    MockUseDataApiUtils.mockQueryError('/api/topics', error)

    const { data, error: queryError } = useQuery('/api/topics')
    expect(queryError).toBe(error)
    expect(data).toBeUndefined()
  })
})

usePreference Hooks Mock

简介

usePreference.ts 提供了偏好设置钩子的统一模拟,支持单个和批量偏好管理。

支持的钩子

  • usePreference - 单个偏好设置钩子
  • useMultiplePreferences - 多个偏好设置钩子

基本使用

import { usePreference, useMultiplePreferences } from '@data/hooks/usePreference'

describe('Preference Hooks', () => {
  it('should work with usePreference', async () => {
    const [theme, setTheme] = usePreference('ui.theme')

    expect(theme).toBe('light') // 默认值

    await setTheme('dark')
    // 在测试中,可以通过工具函数验证值是否更新
  })

  it('should work with multiple preferences', async () => {
    const [prefs, setPrefs] = useMultiplePreferences({
      theme: 'ui.theme',
      lang: 'ui.language'
    })

    expect(prefs.theme).toBe('light')
    expect(prefs.lang).toBe('en')

    await setPrefs({ theme: 'dark' })
  })
})

高级测试

import { MockUsePreferenceUtils } from 'tests/__mocks__/usePreference'

describe('Advanced preference testing', () => {
  beforeEach(() => {
    MockUsePreferenceUtils.resetMocks()
  })

  it('should simulate preference changes', () => {
    MockUsePreferenceUtils.setPreferenceValue('ui.theme', 'dark')

    const [theme] = usePreference('ui.theme')
    expect(theme).toBe('dark')
  })

  it('should simulate external changes', () => {
    let callCount = 0
    MockUsePreferenceUtils.addSubscriber('ui.theme', () => callCount++)

    MockUsePreferenceUtils.simulateExternalPreferenceChange('ui.theme', 'dark')
    expect(callCount).toBe(1)
  })
})

useCache Hooks Mock

简介

useCache.ts 提供了缓存钩子的统一模拟,支持三种缓存层级。

支持的钩子

  • useCache - 内存缓存钩子
  • useSharedCache - 共享缓存钩子
  • usePersistCache - 持久化缓存钩子

基本使用

import { useCache, useSharedCache, usePersistCache } from '@data/hooks/useCache'

describe('Cache Hooks', () => {
  it('should work with useCache', () => {
    const [theme, setTheme] = useCache('ui.theme', 'light')

    expect(theme).toBe('light')
    setTheme('dark')
    // 值立即更新
  })

  it('should work with different cache types', () => {
    const [shared, setShared] = useSharedCache('app.window_count', 1)
    const [persist, setPersist] = usePersistCache('app.last_version', '1.0.0')

    expect(shared).toBe(1)
    expect(persist).toBe('1.0.0')
  })
})

测试工具

import { MockUseCacheUtils } from 'tests/__mocks__/useCache'

describe('Cache hook testing', () => {
  beforeEach(() => {
    MockUseCacheUtils.resetMocks()
  })

  it('should set initial cache state', () => {
    MockUseCacheUtils.setMultipleCacheValues({
      memory: [['ui.theme', 'dark']],
      shared: [['app.mode', 'development']],
      persist: [['user.id', 'user123']]
    })

    const [theme] = useCache('ui.theme')
    const [mode] = useSharedCache('app.mode')
    const [userId] = usePersistCache('user.id')

    expect(theme).toBe('dark')
    expect(mode).toBe('development')
    expect(userId).toBe('user123')
  })
})

LoggerService Mock

简介

项目还包含了 LoggerService 的模拟:

  • RendererLoggerService.ts - 渲染进程日志服务模拟
  • MainLoggerService.ts - 主进程日志服务模拟

这些模拟同样在相应的测试设置文件中全局配置。

最佳实践

  1. 优先使用全局模拟:大多数情况下应该直接使用全局配置的模拟,而不是在每个测试中单独模拟
  2. 合理的默认值:确保模拟的默认值反映实际应用的常见配置
  3. 文档更新:当添加新的模拟或修改现有模拟时,请更新相关文档
  4. 类型安全:保持模拟与实际服务的类型兼容性
  5. 测试隔离:如果需要修改模拟行为,确保在测试后恢复或在 beforeEach 中重置

故障排除

模拟未生效

如果发现 PreferenceService 模拟未生效:

  1. 确认测试运行在渲染进程环境中(vitest.config.ts 中的 renderer 项目)
  2. 检查 tests/renderer.setup.ts 是否正确配置
  3. 确认导入路径使用的是 @data/PreferenceService 而非相对路径

类型错误

如果遇到 TypeScript 类型错误:

  1. 确认模拟实现与实际 PreferenceService 接口匹配
  2. 在测试中使用类型断言:(preferenceService as any)._getMockState()
  3. 检查是否需要更新模拟的类型定义