# 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) ``` ### 原则 1. **安全先行**: 加密/解密模块必须 100% 测试覆盖 2. **核心优先**: SSH 连接逻辑是应用核心,优先覆盖 3. **可 mock 性**: 所有外部依赖(ssh2, better-sqlite3, electron)必须可 mock 4. **独立性**: 每个测试独立运行,不依赖外部状态 5. **快速反馈**: 单元测试 < 5s,集成测试 < 30s --- ## 测试环境配置 ### 已有配置 ```typescript // 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 策略 ```typescript // 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+ ```typescript 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 ```typescript // 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+ ```typescript 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+ ```typescript 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) ```typescript 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+ ```typescript 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+ ```typescript // 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 配置 ```typescript // playwright.config.ts import { defineConfig } from '@playwright/test' export default defineConfig({ testDir: './test/e2e', timeout: 30000, use: { baseURL: 'http://localhost:5173' }, }) ``` ### E2E 用例 - 快速连接 → SSH 终端出现 - 侧边栏会话 → 点击 → 终端连接 - SFTP → 选择服务器 → 浏览目录 - 标签页 → 创建 → 切换 → 关闭 --- ## 测试执行 ### 命令 ```bash npm test # 运行所有测试 npm test -- --coverage # 带覆盖率报告 npm test -- test/services/ # 只跑 services 测试 npm test -- --reporter=verbose # 详细输出 npm test -- --watch # 监听模式 ``` ### CI 集成(规划) ```yaml # .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+** | — |