Skip to content

测试指南

项目已安装 vitest 作为测试框架,当前测试体系处于建设初期:已有 3 个手动 UI 测试页面覆盖关键原生功能,自动化单元/集成测试待补充。

1. 当前测试覆盖

1.1 手动 UI 测试页面

项目包含 3 个专用测试页面,通过路由直接访问:

页面路由测试目标
生物认证测试pages/test/biometric-testFace ID / Touch ID / 指纹认证全流程
LLaMA AI 测试pages/llama-test/index模型下载、加载、推理、桥接
语音识别测试pages/voice-test/index百度语音识别(简易模式 + 自定义 UI)

1.2 生物认证测试项

/src/pages/test/biometric-test.vue 提供 7 个独立测试项:

测试项说明
基础连通测试验证原生桥接模块是否正常加载
检测生物认证可用性检查设备是否支持 Face ID / Touch ID / 指纹
触发生物认证调起系统生物认证弹窗
密钥存储将测试密码写入 Keychain / Keystore
密钥读取从 Keychain / Keystore 读取已存储的密码
清理删除已存储的密钥
一键完整流程顺序执行以上所有测试步骤

1.3 LLaMA AI 测试项

/src/pages/llama-test/index.vue 验证端侧 AI 引擎的完整链路:

  • 模型下载(GGUF 文件获取)
  • 模型加载(Llama.cpp 初始化)
  • 推理测试(发送 prompt 并获取回复)
  • 流式推理测试

1.4 语音识别测试项

/src/pages/voice-test/index.vue 验证百度语音 SDK 集成:

  • 简易模式:调用 plus.speech 默认 UI,验证录音→识别全流程
  • 自定义 UI 模式:使用 plus.speech API 自定义录音界面,验证原始识别结果回调

2. vitest 自动化测试

2.1 安装状态

项目已安装 vitest@^1.3.1 作为 devDependency,但尚未配置测试脚本和配置文件。

2.2 初始化配置

在项目根目录创建 vitest.config.ts

typescript
import { defineConfig } from 'vitest/config'
import { resolve } from 'path'

export default defineConfig({
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
    },
  },
  test: {
    environment: 'node',
    globals: true,
    include: ['src/**/*.{test,spec}.ts'],
  },
})

package.json 中添加测试脚本:

json
{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:coverage": "vitest run --coverage"
  }
}

2.3 推荐测试范围

基于项目核心模块的优先级:

优先级模块测试类型说明
src/utils/crypto.ts单元测试加密/解密/密钥派生正确性
src/database/engines/encrypted-store.ts单元+集成透明加密读写正确性
src/database/base-repository.ts单元测试CRUD 操作正确性
src/sync/sync-manager.ts单元测试队列管理、Provider 切换
src/ai/agent-prompt.ts单元测试System Prompt 组装正确性
src/ai/actions.ts单元测试Action Schema 校验
src/utils/encryption-manager.ts单元测试密码设置/验证/锁定流程
src/stores/单元测试Pinia Store 业务逻辑
src/components/组件测试Vue 组件渲染和交互

2.4 示例:crypto.ts 单测

typescript
// src/utils/__tests__/crypto.test.ts
import { describe, it, expect } from 'vitest'
import { deriveKey, encrypt, decrypt, setActiveKey } from '../crypto'

describe('crypto 模块', () => {
  const password = 'test-password-123'
  const plaintext = { hello: 'world', count: 42 }

  it('PBKDF2 派生密钥', async () => {
    const salt = crypto.getRandomValues(new Uint8Array(32))
    const key = await deriveKey(password, salt)
    expect(key).toBeDefined()
    expect(key.raw.words).toBeDefined()
  })

  it('AES-256-GCM 加密后解密得到原始数据', async () => {
    const salt = crypto.getRandomValues(new Uint8Array(32))
    const key = await deriveKey(password, salt)
    setActiveKey(key)

    const encrypted = await encrypt(JSON.stringify(plaintext))
    const decrypted = await decrypt(key, encrypted._enc, encrypted._iv, encrypted._algo)
    const parsed = JSON.parse(decrypted)

    expect(parsed).toEqual(plaintext)
  })

  it('不同密码派生不同密钥', async () => {
    const salt = crypto.getRandomValues(new Uint8Array(32))
    const key1 = await deriveKey('password1', salt)
    const key2 = await deriveKey('password2', salt)
    // 密钥应不相同
    expect(key1.raw).not.toEqual(key2.raw)
  })
})

2.5 示例:base-repository.ts 单测

typescript
// src/database/__tests__/base-repository.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { BaseRepository } from '../base-repository'

// Mock IStore
class MockStore {
  private data: Map<string, any> = new Map()
  async put(item: any) { this.data.set(item.id, item); return item }
  async get(id: string) { return this.data.get(id) || null }
  async toArray() { return Array.from(this.data.values()) }
  async delete(id: string) { this.data.delete(id) }
  async bulkPut(items: any[]) { items.forEach(i => this.data.set(i.id, i)) }
  async clear() { this.data.clear() }
}

class TestEntity extends BaseRepository<{ id: string; value: string }> {
  constructor() { super('test', new MockStore()) }
}

describe('BaseRepository', () => {
  let repo: TestEntity

  beforeEach(() => { repo = new TestEntity() })

  it('create 自动生成 id 和时间戳', async () => {
    const entity = await repo.create({ value: 'test' })
    expect(entity.id).toBeDefined()
    expect(entity.createdAt).toBeTypeOf('number')
    expect(entity.value).toBe('test')
  })

  it('getById 返回已创建的实体', async () => {
    const created = await repo.create({ value: 'find-me' })
    const found = await repo.getById(created.id)
    expect(found).not.toBeNull()
    expect(found!.value).toBe('find-me')
  })

  it('remove 软删除标记 deleted', async () => {
    const created = await repo.create({ value: 'delete-me' })
    const removed = await repo.remove(created.id)
    expect(removed.deleted).toBe(true)
  })
})

3. 运行测试

bash
# 运行所有测试
npm test

# 监听模式
npm run test:watch

# 单个文件
npx vitest run src/utils/__tests__/crypto.test.ts

# 覆盖率报告
npm run test:coverage

4. 测试约定

约定说明
文件命名*.test.ts*.spec.ts
目录位置与被测文件同级的 __tests__/ 目录
环境纯逻辑测试用 node,需 DOM 的用 jsdom
Mock数据库操作使用 Mock IStore 而非真实引擎
覆盖率目标核心模块 > 80%,工具函数 > 90%