9.9 KiB
TDD_PLAN.md - 测试驱动开发计划
本文档规划 OpsXShell 的测试策略,覆盖从单元测试到 E2E 测试的完整体系。
当前状态
|| 指标 | 当前值 | 目标值 | |------|--------|--------| | 测试文件数 | 5 | 15+ | | 测试用例数 | 72 | 150+ | | 估算覆盖率 | ~15% | ≥50% | | 核心模块覆盖 | 60% | ≥80% |
已有测试
|| 测试文件 | 用例数 | 覆盖模块 | 质量 |
|----------|--------|----------|------|
| test/stores/tabStore.test.ts | 14 | tabStore | ✅ 完整 |
| test/stores/connectionPool.test.ts | 8 | poolStore | 🟡 仅接口验证 |
| test/components/sftp/utils.test.ts | 16 | sftp/utils | ✅ 完整 |
| test/services/crypto.test.ts | 11 | crypto | ✅ 完整 |
| test/stores/keybindingStore.test.ts | 23 | keybindingStore | ✅ 完整 |
测试策略
v1.3.0 测试计划
├── P0: 核心安全模块(crypto, ssh-service)
├── P1: 数据层(database, error-handler, stores)
├── P1: IPC 通信层(8 个 IPC Handler)
├── P2: UI 层(TerminalView, useSFTP hooks)
└── P3: E2E 集成(Playwright)
原则
- 安全先行: 加密/解密模块必须 100% 测试覆盖
- 核心优先: SSH 连接逻辑是应用核心,优先覆盖
- 可 mock 性: 所有外部依赖(ssh2, better-sqlite3, electron)必须可 mock
- 独立性: 每个测试独立运行,不依赖外部状态
- 快速反馈: 单元测试 < 5s,集成测试 < 30s
测试环境配置
已有配置
// vitest.config.ts
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
thresholds: {
statements: 50, // 当前远未达到
branches: 40,
functions: 50,
lines: 50,
},
},
},
})
Mock 策略
// test/setup.ts 已有:
// - window.electronAPI 完整 mock
// - localStorage mock
// 需要新增:
// - ssh2 Client mock
// - better-sqlite3 mock
// - zmodem-ts mock
P0: 核心模块测试(第一批)
1. crypto.ts 测试 — 加密/解密正确性
文件: test/services/crypto.test.ts
用例目标: 15+
describe('Crypto Service', () => {
describe('encrypt / decrypt', () => {
it('应正确加密并解密明文字符串')
it('应正确加密并解密空字符串')
it('应正确加密并解密特殊字符和 Unicode')
it('每次加密应生成不同的密文(随机 IV)')
it('密文篡改后解密应失败(Auth Tag 验证)')
it('错误密钥解密应失败')
})
describe('deriveKey', () => {
it('相同密码和 salt 应派生相同密钥')
it('不同 salt 应派生不同密钥')
it('应使用 100,000 次 PBKDF2 迭代')
})
describe('generateSalt / generateIV', () => {
it('salt 应为 32 字节随机值')
it('IV 应为 12 字节随机值(GCM 推荐)')
it('每次生成的值应不同')
})
})
2. ssh-service.ts 测试 — SSH 连接逻辑
文件: test/services/ssh-service.test.ts
用例目标: 20+
Mock: ssh2 Client
// Mock ssh2
vi.mock('ssh2', () => ({
Client: vi.fn().mockImplementation(() => ({
connect: vi.fn(),
on: vi.fn(),
end: vi.fn(),
shell: vi.fn(),
forwardOut: vi.fn(),
forwardIn: vi.fn(),
sftp: vi.fn(),
})),
}))
describe('SSH Service', () => {
describe('connect', () => {
it('应使用密码认证连接')
it('应使用私钥认证连接')
it('应使用 SSH Agent 认证连接')
it('连接失败应返回错误')
it('连接超时应返回超时错误')
it('重复连接同一主机应复用')
})
describe('createShell', () => {
it('应创建交互式 Shell')
it('应传入终端尺寸 cols/rows')
it('Shell 创建失败应返回错误')
})
describe('disconnect', () => {
it('应正确关闭连接')
it('应清理所有 Shell 会话')
it('应清理端口转发')
})
describe('portForward', () => {
it('应创建本地端口转发')
it('应创建远程端口转发')
it('应创建 SOCKS5 动态代理')
it('端口被占用应返回错误')
})
describe('connectionPool', () => {
it('不应超过最大连接数限制')
it('空闲连接应被自动回收')
it('LRU 淘汰策略应正确工作')
})
})
P1: 数据层 + Store 测试(第二批)
3. database-service.ts 测试
文件: test/services/database.test.ts
用例目标: 15+
describe('Database Service', () => {
describe('Session CRUD', () => {
it('应创建新会话并返回 ID')
it('应获取所有会话列表')
it('应根据 ID 获取单个会话')
it('应更新会话字段')
it('应删除会话')
it('密码字段应加密存储')
it('读取时密码字段应解密')
})
describe('Settings', () => {
it('应保存和读取设置')
it('未设置的键应返回默认值')
it('应支持复杂对象类型设置')
})
describe('数据完整性', () => {
it('数据库文件不存在应自动创建')
it('Schema 版本升级应自动迁移')
})
})
4. error-handler.ts 测试
文件: test/utils/error-handler.test.ts
用例目标: 10+
describe('Error Handler', () => {
describe('inferErrorCode', () => {
it('应从 ECONNREFUSED 推断连接错误')
it('应从 ETIMEDOUT 推断超时错误')
it('应从 auth 推断认证错误')
it('应从 ENOENT 推断文件不存在错误')
it('未知错误应返回通用错误码')
})
describe('withErrorHandling', () => {
it('应包装异步函数并捕获错误')
it('应返回统一错误格式')
})
})
5. poolStore 测试增强
文件: test/stores/connectionPool.test.ts
用例目标: 15+ (新增 8)
describe('Connection Pool Store', () => {
// 已有: 基本接口验证
// 新增:
it('应拒绝超过最大连接数的新连接')
it('LRU 淘汰应移除最久未使用的连接')
it('后台连接超过最大数应断开最旧')
it('活跃连接不应被 LRU 淘汰')
it('连接状态机转换应完整覆盖')
it('挂起超时 30 分钟应彻底断开')
it('保活心跳应重置空闲计时器')
})
6. lifecycleStore 测试
文件: test/stores/lifecycleStore.test.ts
用例目标: 10+
P1: IPC Handler 测试(第三批)
7. SSH IPC 测试
文件: test/ipc/ssh.ipc.test.ts
用例目标: 8+
describe('SSH IPC Handlers', () => {
it('ssh:connect 应调用 sshService.connect')
it('ssh:disconnect 应调用 sshService.disconnect')
it('ssh:create-shell 应调用 sshService.createShell')
it('ssh:shell-write 应调用 sshService.shellWrite')
it('ssh:shell-resize 应调用 sshService.shellResize')
it('ssh:forward-local 应调用 sshService.forwardLocal')
it('ssh:forward-remote 应调用 sshService.forwardRemote')
it('无效参数应返回错误')
})
8. SFTP IPC 测试
文件: test/ipc/sftp.ipc.test.ts
用例目标: 8+
9. Session IPC 测试
文件: test/ipc/session.ipc.test.ts
用例目标: 6+
10. Settings IPC 测试
文件: test/ipc/settings.ipc.test.ts
用例目标: 4+
11. 其他 IPC 测试
文件: test/ipc/{dialog,file,zmodem,app}.ipc.test.ts
用例目标: 每个文件 3-5+
P2: UI 层测试(第四批)
12. TerminalView 组件测试
文件: test/components/TerminalView.test.tsx
用例目标: 10+
// Mock xterm
vi.mock('@xterm/xterm', () => ({
Terminal: vi.fn().mockImplementation(() => ({
onData: vi.fn(), onResize: vi.fn(), open: vi.fn(),
write: vi.fn(), dispose: vi.fn(), cols: 80, rows: 24,
})),
}))
describe('TerminalView', () => {
it('应渲染终端容器')
it('应显示连接中状态')
it('连接成功应初始化 XTerm')
it('连接失败应显示错误信息')
it('Shell 数据应写入终端')
it('终端输入应发送到 Shell')
it('窗口调整应 resize 终端')
it('组件卸载应释放资源')
})
13. useSFTP Hooks 测试
文件: test/hooks/useSFTP.test.ts
用例目标: 15+
14. 其他组件测试
test/components/Sidebar.test.tsx— 5+test/components/SessionDialog.test.tsx— 5+test/components/SFTPView.test.tsx— 5+
P3: E2E 集成测试(远期)
Playwright 配置
// playwright.config.ts
import { defineConfig } from '@playwright/test'
export default defineConfig({
testDir: './test/e2e',
timeout: 30000,
use: { baseURL: 'http://localhost:5173' },
})
E2E 用例
- 快速连接 → SSH 终端出现
- 侧边栏会话 → 点击 → 终端连接
- SFTP → 选择服务器 → 浏览目录
- 标签页 → 创建 → 切换 → 关闭
测试执行
命令
npm test # 运行所有测试
npm test -- --coverage # 带覆盖率报告
npm test -- test/services/ # 只跑 services 测试
npm test -- --reporter=verbose # 详细输出
npm test -- --watch # 监听模式
CI 集成(规划)
# .github/workflows/test.yml
- name: Test
run: npm test -- --coverage
- name: Coverage Check
run: |
if [ $(npm test -- --coverage --reporter=json 2>/dev/null | jq '.total.lines.pct') -lt 50 ]; then
echo "Coverage below 50%"
exit 1
fi
覆盖率追踪
|| 版本 | 总覆盖率 | 核心模块 | 用例数 | 日期 | |------|----------|----------|--------|------| | v1.0.0 | ~10% | 0% | 34 | 2026-04-06 | | v1.1.0 | ~12% | 0% | 34 | — | | v1.2.0 | ~15% | 0% | 40 | — | | v1.3.0 | ~20% | 60% | 49 | 2026-04-24 | | v1.4.0-batch1 | ~25% | 60% | 72 | 2026-04-24 | | v1.3.0-batch2 | ~35% | 60% | 104 | — | | v1.3.0-batch3 | ~45% | 70% | 143 | — | | v1.3.0-final | ≥50% | ≥80% | 150+ | — |