Files
novalon-website/docs/plans/2026-03-29-testing-cicd-optimization.md
T
张翔 8522358427 feat: 提升测试覆盖率并优化测试用例
新增测试:
- use-page-views.test.ts: 测试页面浏览跟踪功能
- api-response.test.ts: 测试API响应辅助函数
- analytics.test.ts: 优化分析函数测试

覆盖率提升:
- branches: 40% -> 41.62%
- functions: 45% -> 47.3%
- lines: 50% -> 52.82%
- statements: 50% -> 51.82%

更新覆盖率阈值到当前水平
2026-03-29 11:48:44 +08:00

20 KiB
Raw Blame History

测试框架与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配置,识别哪些步骤可以并行执行:

# 当前流程(串行)
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步骤前添加:

# ============================================
# 阶段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

  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: 验证配置语法

运行配置验证:

# 验证YAML语法
python -c "import yaml; yaml.safe_load(open('.woodpecker.yml'))"

# 或使用在线YAML验证器

Step 4: 提交更改

git add .woodpecker.yml
git commit -m "feat: 并行化CI代码质量检查步骤

- Lint、Type Check、Security Scan并行执行
- Unit Tests依赖所有检查步骤完成
- 预计减少CI时间50秒"

Task 1.3: 验证并行化效果

Files:

Step 1: 推送更改触发CI

git push origin release/v1.0.0

Step 2: 监控CI执行

访问Pipeline页面,观察:

  • Lint、Type Check、Security Scan是否同时开始执行
  • 记录实际执行时间
  • 对比优化前后的时间差异

Step 3: 记录优化结果

创建监控记录文件:

# 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: 运行覆盖率测试

npm run test:coverage

Step 2: 分析覆盖率报告

打开覆盖率报告:

open coverage/lcov-report/index.html

识别覆盖率较低的模块:

  • 工具函数(utils
  • Hooks
  • API路由

Step 3: 记录当前覆盖率

# 当前测试覆盖率

| 类型 | 当前覆盖率 | 目标覆盖率 | 差距 |
|------|-----------|-----------|------|
| 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: 识别未测试的工具函数

# 查找所有工具函数
find src/lib -name "*.ts" ! -name "*.test.ts" -type f

Step 2: 编写工具函数测试

创建src/lib/utils.test.ts

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: 运行测试验证

npm run test:unit -- src/lib/utils.test.ts

Step 4: 提交更改

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

find src/hooks -name "*.ts" ! -name "*.test.ts" -type f

Step 2: 编写use-debounce Hook测试

创建src/hooks/use-debounce.test.ts

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

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: 运行测试验证

npm run test:unit -- src/hooks/

Step 5: 提交更改

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

coverageThreshold: {
  global: {
    // 阶段1(当前):50%
    // 阶段2(现在):60%
    branches: 60,
    functions: 60,
    lines: 60,
    statements: 60,
  },
},

Step 2: 运行测试验证新阈值

npm run test:coverage:check

Step 3: 提交更改

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

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>): 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>): 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>): 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<T>(
    factory: () => T,
    count: number = 3
  ): T[] {
    return Array.from({ length: count }, factory);
  }
}

Step 2: 安装faker依赖

npm install --save-dev @faker-js/faker

Step 3: 编写测试数据工厂测试

创建src/test-utils/test-data-factory.test.ts

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: 运行测试验证

npm run test:unit -- src/test-utils/

Step 5: 提交更改

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: 识别使用硬编码数据的测试

# 搜索测试中的硬编码数据
grep -r "test@example.com" src/**/*.test.*
grep -r "测试用户" src/**/*.test.*

Step 2: 重构contact路由测试

修改src/app/api/contact/route.test.ts

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

import { TestDataFactory } from '@/test-utils/test-data-factory';

describe('ContactSection', () => {
  it('应该显示联系表单', () => {
    const testUser = TestDataFactory.createUser();

    render(<ContactSection />);

    expect(screen.getByLabelText(/姓名/)).toBeInTheDocument();
    expect(screen.getByLabelText(/邮箱/)).toBeInTheDocument();
  });
});

Step 4: 运行测试验证

npm run test:unit

Step 5: 提交更改

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

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

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: 运行测试验证

npm run test:unit -- src/test-utils/

Step 4: 提交更改

git add src/test-utils/
git commit -m "feat: 创建测试数据清理工具

- 自动清理mock函数
- 清理localStorage和sessionStorage
- 提供autoCleanup装饰器"

验证与总结

Task 4.1: 验证优化效果

Step 1: 运行完整测试套件

npm run test:coverage:check

Step 2: 检查覆盖率报告

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

# 测试框架优化总结报告

## 优化成果

### CI/CD执行时间
- 优化前: ~1180s
- 优化后: ~XXXs
- 改善: XX%

### 测试覆盖率
- 优化前: 50%
- 优化后: 60%
- 改善: +10%

### 测试数据管理
- 创建统一的测试数据工厂
- 实现自动数据清理
- 提高测试可维护性

## 后续计划

### 长期优化(3个月内)
1. 引入视觉回归测试
2. 集成持续性能监控
3. 完善测试文档

Step 5: 提交总结报告

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?