测试指南
项目已安装 vitest 作为测试框架,当前测试体系处于建设初期:已有 3 个手动 UI 测试页面覆盖关键原生功能,自动化单元/集成测试待补充。
1. 当前测试覆盖
1.1 手动 UI 测试页面
项目包含 3 个专用测试页面,通过路由直接访问:
| 页面 | 路由 | 测试目标 |
|---|---|---|
| 生物认证测试 | pages/test/biometric-test | Face 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.speechAPI 自定义录音界面,验证原始识别结果回调
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:coverage4. 测试约定
| 约定 | 说明 |
|---|---|
| 文件命名 | *.test.ts 或 *.spec.ts |
| 目录位置 | 与被测文件同级的 __tests__/ 目录 |
| 环境 | 纯逻辑测试用 node,需 DOM 的用 jsdom |
| Mock | 数据库操作使用 Mock IStore 而非真实引擎 |
| 覆盖率目标 | 核心模块 > 80%,工具函数 > 90% |