# 测试框架与CI/CD持续优化实施计划 > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** 在1个月内完成CI/CD流程并行化、测试覆盖率提升和测试数据管理优化,实现CI执行时间减少60%、测试覆盖率达到60%、测试数据管理标准化。 **Architecture:** 采用渐进式优化策略,优先实施高收益低风险的改进。并行化CI步骤通过Woodpecker CI的depends_on机制实现;测试覆盖率提升通过补充关键模块测试实现;测试数据管理通过创建统一的测试数据工厂实现。 **Tech Stack:** Woodpecker CI, Jest, Playwright, TypeScript, Node.js --- ## 阶段1: CI/CD流程并行化(预计3天) ### Task 1.1: 分析当前CI步骤依赖关系 **Files:** - Analyze: `.woodpecker.yml` **Step 1: 绘制当前CI流程图** 分析当前CI配置,识别哪些步骤可以并行执行: ```yaml # 当前流程(串行) Clone -> Lint -> Type Check -> Security Scan -> Unit Tests -> E2E Tests -> Build -> Deploy # 优化后流程(并行) Clone -> [Lint || Type Check || Security Scan] -> Unit Tests -> E2E Tests -> Build -> Deploy ``` **Step 2: 识别可并行的步骤** 可并行的步骤: - Lint(代码检查) - Type Check(类型检查) - Security Scan(安全扫描) 不可并行的步骤: - Unit Tests(依赖前面的代码质量检查) - E2E Tests(依赖Unit Tests) - Build(依赖所有测试通过) - Deploy(依赖Build成功) **Step 3: 记录优化预期** 预期效果: - 并行化前:Lint(30s) + TypeCheck(40s) + Security(20s) = 90s - 并行化后:max(30s, 40s, 20s) = 40s - 节省时间:50s --- ### Task 1.2: 修改CI配置实现并行化 **Files:** - Modify: `.woodpecker.yml:60-120` **Step 1: 添加并行化配置** 修改`.woodpecker.yml`,在lint、type-check、security-scan步骤前添加: ```yaml # ============================================ # 阶段1: 并行代码质量检查 # ============================================ steps: lint: image: *node_image environment: NODE_ENV: development commands: - npm ci --cache /tmp/npm-cache - npm run lint volumes: - /tmp/npm-cache:/root/.npm - /tmp/node-modules-cache:/woodpecker/src/node_modules when: event: [push, pull_request] branch: [feature/**, dev, release, release/**] type-check: image: *node_image environment: NODE_ENV: development commands: - npm ci --cache /tmp/npm-cache - npm run type-check volumes: - /tmp/npm-cache:/root/.npm - /tmp/node-modules-cache:/woodpecker/src/node_modules when: event: [push, pull_request] branch: [feature/**, dev, release, release/**] security-scan: image: *node_image environment: NODE_ENV: production HUSKY: 0 commands: - npm ci --omit=dev --ignore-scripts --cache /tmp/npm-cache - npm audit --audit-level=high --omit=dev volumes: - /tmp/npm-cache:/root/.npm when: event: [push, pull_request] branch: [feature/**, dev, release, release/**] failure: ignore ``` **Step 2: 添加单元测试依赖配置** 修改unit-tests步骤,添加depends_on: ```yaml unit-tests: image: *node_image environment: NODE_ENV: test CI: true depends_on: [lint, type-check, security-scan] commands: - npm install --cache /tmp/npm-cache - npm run test:coverage:check volumes: - /tmp/npm-cache:/root/.npm - /tmp/node-modules-cache:/woodpecker/src/node_modules when: event: [push, pull_request] branch: [dev, release, release/**] ``` **Step 3: 验证配置语法** 运行配置验证: ```bash # 验证YAML语法 python -c "import yaml; yaml.safe_load(open('.woodpecker.yml'))" # 或使用在线YAML验证器 ``` **Step 4: 提交更改** ```bash git add .woodpecker.yml git commit -m "feat: 并行化CI代码质量检查步骤 - Lint、Type Check、Security Scan并行执行 - Unit Tests依赖所有检查步骤完成 - 预计减少CI时间50秒" ``` --- ### Task 1.3: 验证并行化效果 **Files:** - Monitor: https://ci.f.novalon.cn/repos/1/pipeline/ **Step 1: 推送更改触发CI** ```bash git push origin release/v1.0.0 ``` **Step 2: 监控CI执行** 访问Pipeline页面,观察: - Lint、Type Check、Security Scan是否同时开始执行 - 记录实际执行时间 - 对比优化前后的时间差异 **Step 3: 记录优化结果** 创建监控记录文件: ```markdown # CI并行化优化记录 ## 优化前 - Lint: 30s - Type Check: 40s - Security Scan: 20s - 总计: 90s(串行) ## 优化后 - 并行执行时间: 40s - 节省时间: 50s - 改善比例: 55.6% ``` --- ## 阶段2: 测试覆盖率提升(预计7天) ### Task 2.1: 分析当前测试覆盖率 **Files:** - Analyze: `coverage/lcov-report/index.html` - Modify: `jest.config.js` **Step 1: 运行覆盖率测试** ```bash npm run test:coverage ``` **Step 2: 分析覆盖率报告** 打开覆盖率报告: ```bash open coverage/lcov-report/index.html ``` 识别覆盖率较低的模块: - 工具函数(utils) - Hooks - API路由 **Step 3: 记录当前覆盖率** ```markdown # 当前测试覆盖率 | 类型 | 当前覆盖率 | 目标覆盖率 | 差距 | |------|-----------|-----------|------| | Branches | 40% | 60% | +20% | | Functions | 45% | 60% | +15% | | Lines | 50% | 60% | +10% | | Statements | 50% | 60% | +10% | ``` --- ### Task 2.2: 补充工具函数测试 **Files:** - Create: `src/lib/utils.test.ts` - Modify: `src/lib/utils.ts`(如需) **Step 1: 识别未测试的工具函数** ```bash # 查找所有工具函数 find src/lib -name "*.ts" ! -name "*.test.ts" -type f ``` **Step 2: 编写工具函数测试** 创建`src/lib/utils.test.ts`: ```typescript import { describe, it, expect } from '@jest/globals'; import { cn, formatDate, validateEmail } from './utils'; describe('工具函数测试', () => { describe('cn (className合并)', () => { it('应该正确合并多个className', () => { expect(cn('foo', 'bar')).toBe('foo bar'); }); it('应该处理条件className', () => { expect(cn('foo', false && 'bar', 'baz')).toBe('foo baz'); }); it('应该处理undefined和null', () => { expect(cn('foo', undefined, null, 'bar')).toBe('foo bar'); }); }); describe('formatDate', () => { it('应该正确格式化日期', () => { const date = new Date('2024-01-01'); expect(formatDate(date)).toBe('2024-01-01'); }); it('应该处理无效日期', () => { expect(formatDate(null)).toBe(''); }); }); describe('validateEmail', () => { it('应该验证有效的邮箱地址', () => { expect(validateEmail('test@example.com')).toBe(true); }); it('应该拒绝无效的邮箱地址', () => { expect(validateEmail('invalid-email')).toBe(false); }); }); }); ``` **Step 3: 运行测试验证** ```bash npm run test:unit -- src/lib/utils.test.ts ``` **Step 4: 提交更改** ```bash git add src/lib/utils.test.ts git commit -m "test: 添加工具函数测试用例 - 测试className合并功能 - 测试日期格式化功能 - 测试邮箱验证功能 - 提升覆盖率约5%" ``` --- ### Task 2.3: 补充Hooks测试 **Files:** - Create: `src/hooks/use-debounce.test.ts` - Create: `src/hooks/use-local-storage.test.ts` **Step 1: 识别未测试的Hooks** ```bash find src/hooks -name "*.ts" ! -name "*.test.ts" -type f ``` **Step 2: 编写use-debounce Hook测试** 创建`src/hooks/use-debounce.test.ts`: ```typescript import { describe, it, expect, jest, beforeEach } from '@jest/globals'; import { renderHook, act } from '@testing-library/react'; import { useDebounce } from './use-debounce'; describe('useDebounce Hook', () => { beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); }); it('应该延迟更新值', () => { const { result, rerender } = renderHook( ({ value, delay }) => useDebounce(value, delay), { initialProps: { value: 'initial', delay: 500 } } ); expect(result.current).toBe('initial'); rerender({ value: 'updated', delay: 500 }); expect(result.current).toBe('initial'); act(() => { jest.advanceTimersByTime(500); }); expect(result.current).toBe('updated'); }); it('应该取消之前的定时器', () => { const { result, rerender } = renderHook( ({ value, delay }) => useDebounce(value, delay), { initialProps: { value: 'initial', delay: 500 } } ); rerender({ value: 'updated1', delay: 500 }); rerender({ value: 'updated2', delay: 500 }); act(() => { jest.advanceTimersByTime(500); }); expect(result.current).toBe('updated2'); }); }); ``` **Step 3: 编写use-local-storage Hook测试** 创建`src/hooks/use-local-storage.test.ts`: ```typescript import { describe, it, expect, beforeEach } from '@jest/globals'; import { renderHook, act } from '@testing-library/react'; import { useLocalStorage } from './use-local-storage'; describe('useLocalStorage Hook', () => { beforeEach(() => { localStorage.clear(); }); it('应该从localStorage读取初始值', () => { localStorage.setItem('test-key', JSON.stringify('stored-value')); const { result } = renderHook(() => useLocalStorage('test-key', 'default-value') ); expect(result.current[0]).toBe('stored-value'); }); it('应该使用默认值当localStorage为空', () => { const { result } = renderHook(() => useLocalStorage('test-key', 'default-value') ); expect(result.current[0]).toBe('default-value'); }); it('应该更新localStorage值', () => { const { result } = renderHook(() => useLocalStorage('test-key', 'initial') ); act(() => { result.current[1]('updated'); }); expect(result.current[0]).toBe('updated'); expect(localStorage.getItem('test-key')).toBe(JSON.stringify('updated')); }); }); ``` **Step 4: 运行测试验证** ```bash npm run test:unit -- src/hooks/ ``` **Step 5: 提交更改** ```bash git add src/hooks/*.test.ts git commit -m "test: 添加Hooks测试用例 - 测试useDebounce延迟更新功能 - 测试useLocalStorage持久化功能 - 提升覆盖率约5%" ``` --- ### Task 2.4: 更新覆盖率阈值 **Files:** - Modify: `jest.config.js:18-24` **Step 1: 更新覆盖率阈值配置** 修改`jest.config.js`: ```javascript coverageThreshold: { global: { // 阶段1(当前):50% // 阶段2(现在):60% branches: 60, functions: 60, lines: 60, statements: 60, }, }, ``` **Step 2: 运行测试验证新阈值** ```bash npm run test:coverage:check ``` **Step 3: 提交更改** ```bash git add jest.config.js git commit -m "chore: 提升测试覆盖率阈值到60% - branches: 40% -> 60% - functions: 45% -> 60% - lines: 50% -> 60% - statements: 50% -> 60%" ``` --- ## 阶段3: 测试数据管理优化(预计5天) ### Task 3.1: 创建测试数据工厂 **Files:** - Create: `src/test-utils/test-data-factory.ts` - Create: `src/test-utils/test-data-factory.test.ts` **Step 1: 设计测试数据工厂接口** 创建`src/test-utils/test-data-factory.ts`: ```typescript import { faker } from '@faker-js/faker'; export interface User { id: string; name: string; email: string; role: 'admin' | 'user'; createdAt: Date; } export interface Product { id: string; name: string; description: string; price: number; category: string; } export interface News { id: string; title: string; content: string; author: string; publishedAt: Date; } export class TestDataFactory { static createUser(overrides?: Partial): User { return { id: faker.string.uuid(), name: faker.person.fullName(), email: faker.internet.email(), role: faker.helpers.arrayElement(['admin', 'user']), createdAt: faker.date.past(), ...overrides, }; } static createProduct(overrides?: Partial): Product { return { id: faker.string.uuid(), name: faker.commerce.productName(), description: faker.commerce.productDescription(), price: parseFloat(faker.commerce.price()), category: faker.commerce.department(), ...overrides, }; } static createNews(overrides?: Partial): News { return { id: faker.string.uuid(), title: faker.lorem.sentence(), content: faker.lorem.paragraphs(3), author: faker.person.fullName(), publishedAt: faker.date.recent(), ...overrides, }; } static createMany( factory: () => T, count: number = 3 ): T[] { return Array.from({ length: count }, factory); } } ``` **Step 2: 安装faker依赖** ```bash npm install --save-dev @faker-js/faker ``` **Step 3: 编写测试数据工厂测试** 创建`src/test-utils/test-data-factory.test.ts`: ```typescript import { describe, it, expect } from '@jest/globals'; import { TestDataFactory } from './test-data-factory'; describe('TestDataFactory', () => { describe('createUser', () => { it('应该创建用户对象', () => { const user = TestDataFactory.createUser(); expect(user).toHaveProperty('id'); expect(user).toHaveProperty('name'); expect(user).toHaveProperty('email'); expect(user).toHaveProperty('role'); expect(user).toHaveProperty('createdAt'); }); it('应该支持覆盖属性', () => { const user = TestDataFactory.createUser({ name: '测试用户', role: 'admin', }); expect(user.name).toBe('测试用户'); expect(user.role).toBe('admin'); }); }); describe('createProduct', () => { it('应该创建产品对象', () => { const product = TestDataFactory.createProduct(); expect(product).toHaveProperty('id'); expect(product).toHaveProperty('name'); expect(product).toHaveProperty('price'); expect(typeof product.price).toBe('number'); }); }); describe('createMany', () => { it('应该创建多个对象', () => { const users = TestDataFactory.createMany( TestDataFactory.createUser, 5 ); expect(users).toHaveLength(5); expect(users[0].id).not.toBe(users[1].id); }); }); }); ``` **Step 4: 运行测试验证** ```bash npm run test:unit -- src/test-utils/ ``` **Step 5: 提交更改** ```bash git add src/test-utils/ git commit -m "feat: 创建测试数据工厂 - 支持创建用户、产品、新闻等测试数据 - 支持覆盖默认属性 - 支持批量创建测试数据 - 使用faker生成随机数据" ``` --- ### Task 3.2: 重构现有测试使用数据工厂 **Files:** - Modify: `src/app/api/contact/route.test.ts` - Modify: `src/components/sections/contact-section.test.tsx` **Step 1: 识别使用硬编码数据的测试** ```bash # 搜索测试中的硬编码数据 grep -r "test@example.com" src/**/*.test.* grep -r "测试用户" src/**/*.test.* ``` **Step 2: 重构contact路由测试** 修改`src/app/api/contact/route.test.ts`: ```typescript import { describe, it, expect } from '@jest/globals'; import { TestDataFactory } from '@/test-utils/test-data-factory'; describe('Contact API Route', () => { it('应该处理联系表单提交', async () => { const contactData = { name: TestDataFactory.createUser().name, email: TestDataFactory.createUser().email, message: '测试消息', }; const response = await fetch('/api/contact', { method: 'POST', body: JSON.stringify(contactData), }); expect(response.status).toBe(200); }); }); ``` **Step 3: 重构contact-section组件测试** 修改`src/components/sections/contact-section.test.tsx`: ```typescript import { TestDataFactory } from '@/test-utils/test-data-factory'; describe('ContactSection', () => { it('应该显示联系表单', () => { const testUser = TestDataFactory.createUser(); render(); expect(screen.getByLabelText(/姓名/)).toBeInTheDocument(); expect(screen.getByLabelText(/邮箱/)).toBeInTheDocument(); }); }); ``` **Step 4: 运行测试验证** ```bash npm run test:unit ``` **Step 5: 提交更改** ```bash git add src/app/api/contact/route.test.ts src/components/sections/contact-section.test.tsx git commit -m "refactor: 使用测试数据工厂重构测试 - 移除硬编码测试数据 - 使用TestDataFactory生成随机数据 - 提高测试可维护性" ``` --- ### Task 3.3: 创建测试数据清理工具 **Files:** - Create: `src/test-utils/test-data-cleaner.ts` - Create: `src/test-utils/test-data-cleaner.test.ts` **Step 1: 创建测试数据清理工具** 创建`src/test-utils/test-data-cleaner.ts`: ```typescript import { jest } from '@jest/globals'; export class TestDataCleaner { private static mocks: jest.Mock[] = []; static registerMock(mock: jest.Mock): void { this.mocks.push(mock); } static clearAllMocks(): void { this.mocks.forEach(mock => mock.mockClear()); this.mocks = []; } static resetAllMocks(): void { this.mocks.forEach(mock => mock.mockReset()); this.mocks = []; } static cleanup(): void { this.clearAllMocks(); localStorage.clear(); sessionStorage.clear(); } } export function autoCleanup() { afterEach(() => { TestDataCleaner.cleanup(); }); } ``` **Step 2: 编写清理工具测试** 创建`src/test-utils/test-data-cleaner.test.ts`: ```typescript import { describe, it, expect, jest, beforeEach } from '@jest/globals'; import { TestDataCleaner, autoCleanup } from './test-data-cleaner'; describe('TestDataCleaner', () => { beforeEach(() => { TestDataCleaner.cleanup(); }); it('应该注册和清理mock', () => { const mock = jest.fn(); TestDataCleaner.registerMock(mock); mock(); expect(mock).toHaveBeenCalledTimes(1); TestDataCleaner.clearAllMocks(); expect(mock).toHaveBeenCalledTimes(0); }); it('应该清理localStorage', () => { localStorage.setItem('test', 'value'); TestDataCleaner.cleanup(); expect(localStorage.getItem('test')).toBeNull(); }); }); ``` **Step 3: 运行测试验证** ```bash npm run test:unit -- src/test-utils/ ``` **Step 4: 提交更改** ```bash git add src/test-utils/ git commit -m "feat: 创建测试数据清理工具 - 自动清理mock函数 - 清理localStorage和sessionStorage - 提供autoCleanup装饰器" ``` --- ## 验证与总结 ### Task 4.1: 验证优化效果 **Step 1: 运行完整测试套件** ```bash npm run test:coverage:check ``` **Step 2: 检查覆盖率报告** ```bash open coverage/lcov-report/index.html ``` 验证覆盖率是否达到60%目标。 **Step 3: 监控CI执行时间** 访问 https://ci.f.novalon.cn/repos/1/pipeline/ 记录最新的CI执行时间,对比优化前后的改善。 **Step 4: 创建优化总结报告** 创建`docs/testing/optimization-report-2026-03.md`: ```markdown # 测试框架优化总结报告 ## 优化成果 ### CI/CD执行时间 - 优化前: ~1180s - 优化后: ~XXXs - 改善: XX% ### 测试覆盖率 - 优化前: 50% - 优化后: 60% - 改善: +10% ### 测试数据管理 - 创建统一的测试数据工厂 - 实现自动数据清理 - 提高测试可维护性 ## 后续计划 ### 长期优化(3个月内) 1. 引入视觉回归测试 2. 集成持续性能监控 3. 完善测试文档 ``` **Step 5: 提交总结报告** ```bash git add docs/testing/optimization-report-2026-03.md git commit -m "docs: 添加测试框架优化总结报告" ``` --- ## 执行选项 **Plan complete and saved to `docs/plans/2026-03-29-testing-cicd-optimization.md`.** **Two execution options:** **1. Subagent-Driven (this session)** - 我将在当前会话中逐任务执行,每个任务完成后进行代码审查,快速迭代。 **2. Parallel Session (separate)** - 在新的会话中使用executing-plans skill批量执行,设置检查点。 **Which approach?**