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

905 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 测试框架与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>): 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依赖**
```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(<ContactSection />);
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?**