08ea5fbe98
添加用户管理视图、API和状态管理文件
2052 lines
50 KiB
Markdown
2052 lines
50 KiB
Markdown
# 自动化测试流程框架实施计划
|
||
|
||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||
|
||
**Goal:** 建立一套完整的自动化测试流程框架,实现智能测试选择、容器化测试环境、多层次报告体系和企业微信缺陷管理集成。
|
||
|
||
**Architecture:** 采用容器化测试环境(Docker Compose),基于代码变更分析的智能测试选择器,多层次报告生成器(实时/汇总/趋势),与企业微信智能表格集成的缺陷管理闭环。
|
||
|
||
**Tech Stack:**
|
||
- 测试框架:Playwright + TypeScript
|
||
- 容器化:Docker + Docker Compose
|
||
- CI/CD:Woodpecker CI
|
||
- 缺陷管理:企业微信智能表格 + Webhook
|
||
- 数据库:PostgreSQL(复用postgresql_dev)
|
||
|
||
---
|
||
|
||
## 📋 实施阶段概览
|
||
|
||
本实施计划分为4个阶段,共约6周时间:
|
||
|
||
- **阶段1**(第1-2周):基础框架搭建
|
||
- **阶段2**(第3-4周):报告体系与缺陷管理
|
||
- **阶段3**(第5-6周):优化与完善
|
||
- **阶段4**(长期):持续优化
|
||
|
||
---
|
||
|
||
## 阶段1:基础框架搭建(第1-2周)
|
||
|
||
### 任务1.1:创建测试环境配置文件
|
||
|
||
**目标**:创建容器化测试环境的Docker Compose配置文件
|
||
|
||
**Files:**
|
||
- Create: `docker-compose.test.yml`
|
||
- Create: `everything-is-suitable-admin/Dockerfile.test`
|
||
- Create: `.env.test`
|
||
|
||
**Step 1: 创建docker-compose.test.yml文件**
|
||
|
||
```yaml
|
||
# docker-compose.test.yml
|
||
version: '3.8'
|
||
|
||
services:
|
||
# 前端应用(测试环境)
|
||
admin-frontend-test:
|
||
build:
|
||
context: ./everything-is-suitable-admin
|
||
dockerfile: Dockerfile.test
|
||
container_name: admin-frontend-test
|
||
ports:
|
||
- "5174:5174"
|
||
environment:
|
||
- NODE_ENV=test
|
||
- VITE_API_BASE_URL=http://admin-api-test:8082
|
||
- VITE_MOCK_ENABLED=false
|
||
depends_on:
|
||
admin-api-test:
|
||
condition: service_healthy
|
||
networks:
|
||
- test-network
|
||
healthcheck:
|
||
test: ["CMD", "curl", "-f", "http://localhost:5174"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 3
|
||
|
||
# 后端API(测试环境)
|
||
admin-api-test:
|
||
build:
|
||
context: ./everything-is-suitable-api/everything-is-suitable-admin-app
|
||
dockerfile: Dockerfile
|
||
container_name: admin-api-test
|
||
ports:
|
||
- "8083:8082"
|
||
environment:
|
||
- SPRING_PROFILES_ACTIVE=test
|
||
- SPRING_R2DBC_URL=r2dbc:postgresql://host.docker.internal:5432/everything_suitable_test
|
||
- SPRING_R2DBC_USERNAME=${DB_USERNAME:-postgres}
|
||
- SPRING_R2DBC_PASSWORD=${DB_PASSWORD:-postgres}
|
||
networks:
|
||
- test-network
|
||
extra_hosts:
|
||
- "host.docker.internal:host-gateway"
|
||
healthcheck:
|
||
test: ["CMD", "curl", "-f", "http://localhost:8082/actuator/health"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 5
|
||
|
||
networks:
|
||
test-network:
|
||
driver: bridge
|
||
```
|
||
|
||
**Step 2: 创建前端测试Dockerfile**
|
||
|
||
```dockerfile
|
||
# everything-is-suitable-admin/Dockerfile.test
|
||
FROM node:18-alpine
|
||
|
||
WORKDIR /app
|
||
|
||
# 复制package文件
|
||
COPY package*.json ./
|
||
|
||
# 安装依赖
|
||
RUN npm ci
|
||
|
||
# 复制源代码
|
||
COPY . .
|
||
|
||
# 构建应用
|
||
RUN npm run build
|
||
|
||
# 使用nginx提供静态文件服务
|
||
FROM nginx:alpine
|
||
COPY --from=0 /app/dist /usr/share/nginx/html
|
||
COPY nginx.conf /etc/nginx/nginx.conf
|
||
|
||
EXPOSE 5174
|
||
|
||
CMD ["nginx", "-g", "daemon off;"]
|
||
```
|
||
|
||
**Step 3: 创建测试环境变量文件**
|
||
|
||
```bash
|
||
# .env.test
|
||
NODE_ENV=test
|
||
TEST_ENV=ci
|
||
|
||
# 测试环境URL
|
||
API_BASE_URL=http://localhost:8083
|
||
FRONTEND_BASE_URL=http://localhost:5174
|
||
|
||
# 数据库配置
|
||
DB_HOST=localhost
|
||
DB_PORT=5432
|
||
DB_NAME=everything_suitable_test
|
||
DB_USERNAME=postgres
|
||
DB_PASSWORD=postgres
|
||
|
||
# 企业微信配置
|
||
WECOM_WEBHOOK_URL=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY
|
||
WECOM_TABLE_ID=YOUR_TABLE_ID
|
||
WECOM_BOT_WEBHOOK=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_BOT_KEY
|
||
```
|
||
|
||
**Step 4: 验证配置文件语法**
|
||
|
||
Run: `docker-compose -f docker-compose.test.yml config`
|
||
|
||
Expected: 配置文件语法正确,无错误输出
|
||
|
||
**Step 5: 提交配置文件**
|
||
|
||
```bash
|
||
git add docker-compose.test.yml everything-is-suitable-admin/Dockerfile.test .env.test
|
||
git commit -m "feat: add test environment docker compose configuration"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务1.2:配置测试数据库
|
||
|
||
**目标**:创建测试数据库并初始化测试数据
|
||
|
||
**Files:**
|
||
- Create: `scripts/init-test-database.sh`
|
||
- Create: `scripts/init-test-data.ts`
|
||
|
||
**Step 1: 创建数据库初始化脚本**
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
# scripts/init-test-database.sh
|
||
|
||
set -e
|
||
|
||
echo "=== 初始化测试数据库 ==="
|
||
|
||
# 检查postgresql_dev容器是否运行
|
||
if ! docker ps | grep -q postgresql_dev; then
|
||
echo "❌ postgresql_dev容器未运行"
|
||
echo "请先启动容器: docker start postgresql_dev"
|
||
exit 1
|
||
fi
|
||
|
||
# 创建测试数据库
|
||
echo "创建测试数据库..."
|
||
docker exec postgresql_dev psql -U postgres -c "CREATE DATABASE everything_suitable_test;" || echo "数据库已存在"
|
||
|
||
# 创建测试Schema
|
||
echo "创建测试Schema..."
|
||
docker exec postgresql_dev psql -U postgres -d everything_suitable_test -c "CREATE SCHEMA IF NOT EXISTS test_data;"
|
||
|
||
echo "✅ 测试数据库初始化完成"
|
||
```
|
||
|
||
**Step 2: 创建测试数据初始化脚本**
|
||
|
||
```typescript
|
||
// scripts/init-test-data.ts
|
||
import { Pool } from 'pg';
|
||
|
||
const pool = new Pool({
|
||
host: process.env.DB_HOST || 'localhost',
|
||
port: parseInt(process.env.DB_PORT || '5432'),
|
||
database: process.env.DB_NAME || 'everything_suitable_test',
|
||
user: process.env.DB_USERNAME || 'postgres',
|
||
password: process.env.DB_PASSWORD || 'postgres',
|
||
});
|
||
|
||
async function initTestData() {
|
||
const client = await pool.connect();
|
||
|
||
try {
|
||
await client.query('BEGIN');
|
||
|
||
// 清理测试数据
|
||
console.log('清理测试数据...');
|
||
await client.query('TRUNCATE TABLE test_data.users CASCADE');
|
||
await client.query('TRUNCATE TABLE test_data.roles CASCADE');
|
||
await client.query('TRUNCATE TABLE test_data.menus CASCADE');
|
||
|
||
// 创建测试用户
|
||
console.log('创建测试用户...');
|
||
await client.query(`
|
||
INSERT INTO test_data.users (username, password, email, status) VALUES
|
||
('admin', 'admin123', 'admin@example.com', 'active'),
|
||
('user1', 'user123', 'user1@example.com', 'active'),
|
||
('user2', 'user123', 'user2@example.com', 'active')
|
||
`);
|
||
|
||
// 创建测试角色
|
||
console.log('创建测试角色...');
|
||
await client.query(`
|
||
INSERT INTO test_data.roles (name, code, status) VALUES
|
||
('管理员', 'admin', 1),
|
||
('普通用户', 'user', 1)
|
||
`);
|
||
|
||
// 创建测试菜单
|
||
console.log('创建测试菜单...');
|
||
await client.query(`
|
||
INSERT INTO test_data.menus (name, path, type, status) VALUES
|
||
('用户管理', '/user-management', 1, 0),
|
||
('角色管理', '/role-management', 1, 0),
|
||
('菜单管理', '/menu-management', 1, 0)
|
||
`);
|
||
|
||
await client.query('COMMIT');
|
||
console.log('✅ 测试数据初始化完成');
|
||
} catch (error) {
|
||
await client.query('ROLLBACK');
|
||
console.error('❌ 测试数据初始化失败:', error);
|
||
throw error;
|
||
} finally {
|
||
client.release();
|
||
}
|
||
}
|
||
|
||
initTestData()
|
||
.then(() => process.exit(0))
|
||
.catch((error) => {
|
||
console.error(error);
|
||
process.exit(1);
|
||
});
|
||
```
|
||
|
||
**Step 3: 运行数据库初始化脚本**
|
||
|
||
Run: `chmod +x scripts/init-test-database.sh && ./scripts/init-test-database.sh`
|
||
|
||
Expected: 测试数据库创建成功
|
||
|
||
**Step 4: 安装pg依赖并运行测试数据初始化**
|
||
|
||
Run: `cd scripts && npm install pg && ts-node init-test-data.ts`
|
||
|
||
Expected: 测试数据初始化成功
|
||
|
||
**Step 5: 提交数据库初始化脚本**
|
||
|
||
```bash
|
||
git add scripts/init-test-database.sh scripts/init-test-data.ts
|
||
git commit -m "feat: add test database initialization scripts"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务1.3:创建代码-测试映射配置
|
||
|
||
**目标**:建立代码文件与测试用例之间的映射关系
|
||
|
||
**Files:**
|
||
- Create: `config/test-mapping.config.ts`
|
||
|
||
**Step 1: 创建映射配置文件**
|
||
|
||
```typescript
|
||
// config/test-mapping.config.ts
|
||
export interface TestMapping {
|
||
[sourceFile: string]: {
|
||
tests: string[];
|
||
priority: 'high' | 'medium' | 'low';
|
||
modules: string[];
|
||
};
|
||
}
|
||
|
||
export const testMapping: TestMapping = {
|
||
// 用户管理模块
|
||
'everything-is-suitable-admin/src/views/UserManagement.vue': {
|
||
tests: [
|
||
'e2e/user-management/*.spec.ts',
|
||
],
|
||
priority: 'high',
|
||
modules: ['user-management'],
|
||
},
|
||
'everything-is-suitable-admin/src/api/user.ts': {
|
||
tests: [
|
||
'e2e/user-management/*.spec.ts',
|
||
'e2e/api/user-api.spec.ts',
|
||
],
|
||
priority: 'high',
|
||
modules: ['user-management', 'api'],
|
||
},
|
||
'everything-is-suitable-admin/src/stores/user.ts': {
|
||
tests: [
|
||
'e2e/user-management/*.spec.ts',
|
||
],
|
||
priority: 'medium',
|
||
modules: ['user-management'],
|
||
},
|
||
|
||
// 角色管理模块
|
||
'everything-is-suitable-admin/src/views/RoleManagement.vue': {
|
||
tests: [
|
||
'e2e/role-management/*.spec.ts',
|
||
],
|
||
priority: 'high',
|
||
modules: ['role-management'],
|
||
},
|
||
'everything-is-suitable-admin/src/api/role.ts': {
|
||
tests: [
|
||
'e2e/role-management/*.spec.ts',
|
||
'e2e/api/role-api.spec.ts',
|
||
],
|
||
priority: 'high',
|
||
modules: ['role-management', 'api'],
|
||
},
|
||
|
||
// 菜单管理模块
|
||
'everything-is-suitable-admin/src/views/MenuManagement.vue': {
|
||
tests: [
|
||
'e2e/menu-management/*.spec.ts',
|
||
],
|
||
priority: 'high',
|
||
modules: ['menu-management'],
|
||
},
|
||
'everything-is-suitable-admin/src/api/menu.ts': {
|
||
tests: [
|
||
'e2e/menu-management/*.spec.ts',
|
||
'e2e/api/menu-api.spec.ts',
|
||
],
|
||
priority: 'high',
|
||
modules: ['menu-management', 'api'],
|
||
},
|
||
|
||
// 黄历功能模块
|
||
'everything-is-suitable-uniapp/src/pages/almanac/index.vue': {
|
||
tests: [
|
||
'e2e/almanac-functionality/*.spec.ts',
|
||
],
|
||
priority: 'high',
|
||
modules: ['almanac-functionality'],
|
||
},
|
||
};
|
||
|
||
// 反向映射:模块 -> 测试文件
|
||
export const moduleToTests: Record<string, string[]> = {
|
||
'user-management': ['e2e/user-management/*.spec.ts'],
|
||
'role-management': ['e2e/role-management/*.spec.ts'],
|
||
'menu-management': ['e2e/menu-management/*.spec.ts'],
|
||
'almanac-functionality': ['e2e/almanac-functionality/*.spec.ts'],
|
||
'api': ['e2e/api/*.spec.ts'],
|
||
};
|
||
```
|
||
|
||
**Step 2: 验证配置文件语法**
|
||
|
||
Run: `npx tsc --noEmit config/test-mapping.config.ts`
|
||
|
||
Expected: 无类型错误
|
||
|
||
**Step 3: 提交映射配置文件**
|
||
|
||
```bash
|
||
git add config/test-mapping.config.ts
|
||
git commit -m "feat: add test mapping configuration"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务1.4:实现智能测试选择器
|
||
|
||
**目标**:实现基于代码变更的智能测试选择器
|
||
|
||
**Files:**
|
||
- Create: `scripts/smart-test-selector.ts`
|
||
- Create: `scripts/cli/smart-test-selector-cli.ts`
|
||
|
||
**Step 1: 创建智能测试选择器核心类**
|
||
|
||
```typescript
|
||
// scripts/smart-test-selector.ts
|
||
import { execSync } from 'child_process';
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
import { testMapping, moduleToTests } from '../config/test-mapping.config';
|
||
|
||
export interface TestSelectionResult {
|
||
selectedTests: string[];
|
||
affectedModules: string[];
|
||
changedFiles: string[];
|
||
analysisReport: string;
|
||
}
|
||
|
||
export class SmartTestSelector {
|
||
private projectRoot: string;
|
||
|
||
constructor(projectRoot: string = process.cwd()) {
|
||
this.projectRoot = projectRoot;
|
||
}
|
||
|
||
/**
|
||
* 根据代码变更选择测试用例
|
||
*/
|
||
selectTestsByChanges(
|
||
changedFiles: string[],
|
||
options: {
|
||
includeRelated?: boolean;
|
||
priority?: 'high' | 'medium' | 'low' | 'all';
|
||
testLevel?: 'smoke' | 'functional' | 'all';
|
||
} = {}
|
||
): TestSelectionResult {
|
||
const {
|
||
includeRelated = true,
|
||
priority = 'all',
|
||
testLevel = 'all',
|
||
} = options;
|
||
|
||
const selectedTests = new Set<string>();
|
||
const affectedModules = new Set<string>();
|
||
|
||
// 分析每个变更文件
|
||
for (const file of changedFiles) {
|
||
const normalizedPath = this.normalizePath(file);
|
||
const mapping = this.findMapping(normalizedPath);
|
||
|
||
if (mapping) {
|
||
// 添加直接关联的测试
|
||
mapping.tests.forEach(test => selectedTests.add(test));
|
||
mapping.modules.forEach(module => affectedModules.add(module));
|
||
|
||
// 如果启用关联分析,添加相关模块的测试
|
||
if (includeRelated) {
|
||
this.addRelatedTests(mapping.modules, selectedTests, affectedModules);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 根据优先级过滤
|
||
const filteredTests = this.filterByPriority(
|
||
Array.from(selectedTests),
|
||
priority
|
||
);
|
||
|
||
// 根据测试级别过滤
|
||
const finalTests = this.filterByTestLevel(filteredTests, testLevel);
|
||
|
||
// 生成分析报告
|
||
const analysisReport = this.generateAnalysisReport({
|
||
changedFiles,
|
||
affectedModules: Array.from(affectedModules),
|
||
selectedTests: finalTests,
|
||
});
|
||
|
||
return {
|
||
selectedTests: finalTests,
|
||
affectedModules: Array.from(affectedModules),
|
||
changedFiles,
|
||
analysisReport,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 从Git获取变更文件
|
||
*/
|
||
getChangedFilesFromGit(
|
||
baseBranch: string = 'origin/main',
|
||
headBranch: string = 'HEAD'
|
||
): string[] {
|
||
try {
|
||
const output = execSync(
|
||
`git diff --name-only ${baseBranch}...${headBranch}`,
|
||
{ encoding: 'utf-8', cwd: this.projectRoot }
|
||
);
|
||
return output
|
||
.split('\n')
|
||
.filter(file => file.trim() && this.isSourceFile(file));
|
||
} catch (error) {
|
||
console.error('Failed to get changed files from git:', error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 规范化文件路径
|
||
*/
|
||
private normalizePath(filePath: string): string {
|
||
return filePath.replace(/\\/g, '/').replace(/^\.\//, '');
|
||
}
|
||
|
||
/**
|
||
* 查找文件对应的测试映射
|
||
*/
|
||
private findMapping(normalizedPath: string) {
|
||
// 精确匹配
|
||
if (testMapping[normalizedPath]) {
|
||
return testMapping[normalizedPath];
|
||
}
|
||
|
||
// 模糊匹配(支持通配符)
|
||
for (const [pattern, mapping] of Object.entries(testMapping)) {
|
||
if (this.matchPattern(normalizedPath, pattern)) {
|
||
return mapping;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 简单的模式匹配
|
||
*/
|
||
private matchPattern(path: string, pattern: string): boolean {
|
||
const regex = new RegExp(
|
||
'^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$'
|
||
);
|
||
return regex.test(path);
|
||
}
|
||
|
||
/**
|
||
* 添加相关测试
|
||
*/
|
||
private addRelatedTests(
|
||
modules: string[],
|
||
selectedTests: Set<string>,
|
||
affectedModules: Set<string>
|
||
): void {
|
||
for (const module of modules) {
|
||
const relatedTests = moduleToTests[module];
|
||
if (relatedTests) {
|
||
relatedTests.forEach(test => selectedTests.add(test));
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 根据优先级过滤测试
|
||
*/
|
||
private filterByPriority(
|
||
tests: string[],
|
||
priority: 'high' | 'medium' | 'low' | 'all'
|
||
): string[] {
|
||
if (priority === 'all') {
|
||
return tests;
|
||
}
|
||
|
||
const priorityMap = {
|
||
high: ['@p0', '@smoke'],
|
||
medium: ['@p1', '@functional'],
|
||
low: ['@p2', '@edge'],
|
||
};
|
||
|
||
const targetTags = priorityMap[priority];
|
||
return tests.filter(test =>
|
||
targetTags.some(tag => test.includes(tag))
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 根据测试级别过滤
|
||
*/
|
||
private filterByTestLevel(
|
||
tests: string[],
|
||
level: 'smoke' | 'functional' | 'all'
|
||
): string[] {
|
||
if (level === 'all') {
|
||
return tests;
|
||
}
|
||
|
||
const levelMap = {
|
||
smoke: '@smoke',
|
||
functional: '@functional',
|
||
};
|
||
|
||
const targetTag = levelMap[level];
|
||
return tests.filter(test => test.includes(targetTag));
|
||
}
|
||
|
||
/**
|
||
* 判断是否为源代码文件
|
||
*/
|
||
private isSourceFile(filePath: string): boolean {
|
||
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.vue', '.java'];
|
||
return extensions.some(ext => filePath.endsWith(ext));
|
||
}
|
||
|
||
/**
|
||
* 生成分析报告
|
||
*/
|
||
private generateAnalysisReport(data: {
|
||
changedFiles: string[];
|
||
affectedModules: string[];
|
||
selectedTests: string[];
|
||
}): string {
|
||
return `
|
||
# 智能测试选择分析报告
|
||
|
||
## 变更文件 (${data.changedFiles.length}个)
|
||
${data.changedFiles.map(f => `- ${f}`).join('\n')}
|
||
|
||
## 受影响模块 (${data.affectedModules.length}个)
|
||
${data.affectedModules.map(m => `- ${m}`).join('\n')}
|
||
|
||
## 选中测试用例 (${data.selectedTests.length}个)
|
||
${data.selectedTests.map(t => `- ${t}`).join('\n')}
|
||
|
||
## 执行建议
|
||
- 优先执行冒烟测试(@smoke标签)
|
||
- 然后执行功能测试(@functional标签)
|
||
- 最后执行边缘场景测试(@edge标签)
|
||
`.trim();
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: 创建CLI工具**
|
||
|
||
```typescript
|
||
// scripts/cli/smart-test-selector-cli.ts
|
||
import * as fs from 'fs';
|
||
import * as yargs from 'yargs';
|
||
import { SmartTestSelector } from '../smart-test-selector';
|
||
|
||
const argv = yargs
|
||
.option('input', {
|
||
alias: 'i',
|
||
type: 'string',
|
||
description: '变更文件列表文件路径',
|
||
})
|
||
.option('output', {
|
||
alias: 'o',
|
||
type: 'string',
|
||
description: '输出文件路径',
|
||
default: 'selected-tests.json',
|
||
})
|
||
.option('report', {
|
||
alias: 'r',
|
||
type: 'string',
|
||
description: '分析报告输出路径',
|
||
default: 'test-selection-report.md',
|
||
})
|
||
.option('priority', {
|
||
alias: 'p',
|
||
type: 'string',
|
||
choices: ['high', 'medium', 'low', 'all'],
|
||
default: 'all',
|
||
description: '测试优先级过滤',
|
||
})
|
||
.option('level', {
|
||
alias: 'l',
|
||
type: 'string',
|
||
choices: ['smoke', 'functional', 'all'],
|
||
default: 'all',
|
||
description: '测试级别过滤',
|
||
})
|
||
.argv as any;
|
||
|
||
async function main() {
|
||
const selector = new SmartTestSelector();
|
||
|
||
let changedFiles: string[] = [];
|
||
|
||
if (argv.input) {
|
||
// 从文件读取变更文件列表
|
||
const content = fs.readFileSync(argv.input, 'utf-8');
|
||
changedFiles = content.split('\n').filter(f => f.trim());
|
||
} else {
|
||
// 从Git获取变更文件
|
||
changedFiles = selector.getChangedFilesFromGit();
|
||
}
|
||
|
||
console.log(`📊 分析 ${changedFiles.length} 个变更文件...`);
|
||
|
||
const result = selector.selectTestsByChanges(changedFiles, {
|
||
priority: argv.priority,
|
||
testLevel: argv.level,
|
||
});
|
||
|
||
// 保存结果
|
||
fs.writeFileSync(argv.output, JSON.stringify(result, null, 2));
|
||
console.log(`✅ 测试选择结果已保存到: ${argv.output}`);
|
||
|
||
// 保存报告
|
||
fs.writeFileSync(argv.report, result.analysisReport);
|
||
console.log(`✅ 分析报告已保存到: ${argv.report}`);
|
||
|
||
// 输出摘要
|
||
console.log('\n=== 选择结果摘要 ===');
|
||
console.log(`变更文件: ${result.changedFiles.length} 个`);
|
||
console.log(`受影响模块: ${result.affectedModules.length} 个`);
|
||
console.log(`选中测试: ${result.selectedTests.length} 个`);
|
||
}
|
||
|
||
main().catch(console.error);
|
||
```
|
||
|
||
**Step 3: 编译TypeScript文件**
|
||
|
||
Run: `npx tsc scripts/smart-test-selector.ts scripts/cli/smart-test-selector-cli.ts --outDir dist/scripts`
|
||
|
||
Expected: 编译成功,无错误
|
||
|
||
**Step 4: 测试智能测试选择器**
|
||
|
||
Run: `node dist/scripts/cli/smart-test-selector-cli.js --help`
|
||
|
||
Expected: 显示CLI帮助信息
|
||
|
||
**Step 5: 提交智能测试选择器代码**
|
||
|
||
```bash
|
||
git add scripts/smart-test-selector.ts scripts/cli/smart-test-selector-cli.ts
|
||
git commit -m "feat: implement smart test selector"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务1.5:创建测试执行脚本
|
||
|
||
**目标**:创建测试执行脚本,支持智能测试和全量测试
|
||
|
||
**Files:**
|
||
- Create: `scripts/run-selected-tests.ts`
|
||
- Create: `scripts/run-all-tests.ts`
|
||
|
||
**Step 1: 创建智能测试执行脚本**
|
||
|
||
```typescript
|
||
// scripts/run-selected-tests.ts
|
||
import { execSync } from 'child_process';
|
||
import * as fs from 'fs';
|
||
|
||
interface SelectedTests {
|
||
smoke: string[];
|
||
functional: string[];
|
||
edge: string[];
|
||
}
|
||
|
||
export class TestExecutor {
|
||
/**
|
||
* 执行智能选择的测试
|
||
*/
|
||
async runSelectedTests(testsFile: string): Promise<void> {
|
||
const selectedTests: SelectedTests = JSON.parse(
|
||
fs.readFileSync(testsFile, 'utf-8')
|
||
);
|
||
|
||
console.log('=== 开始执行智能测试 ===\n');
|
||
|
||
// 1. 执行冒烟测试(优先级最高)
|
||
if (selectedTests.smoke.length > 0) {
|
||
console.log('📦 执行冒烟测试...');
|
||
await this.runTests(selectedTests.smoke, 'smoke');
|
||
}
|
||
|
||
// 2. 执行功能测试
|
||
if (selectedTests.functional.length > 0) {
|
||
console.log('📦 执行功能测试...');
|
||
await this.runTests(selectedTests.functional, 'functional');
|
||
}
|
||
|
||
// 3. 执行边缘场景测试(可选)
|
||
if (selectedTests.edge.length > 0 && process.env.RUN_EDGE_TESTS === 'true') {
|
||
console.log('📦 执行边缘场景测试...');
|
||
await this.runTests(selectedTests.edge, 'edge');
|
||
}
|
||
|
||
console.log('\n✅ 智能测试执行完成');
|
||
}
|
||
|
||
/**
|
||
* 执行指定测试用例
|
||
*/
|
||
private async runTests(
|
||
testPatterns: string[],
|
||
level: string
|
||
): Promise<void> {
|
||
for (const pattern of testPatterns) {
|
||
try {
|
||
console.log(` 执行: ${pattern}`);
|
||
execSync(
|
||
`npx playwright test "${pattern}" --project=chromium --reporter=html`,
|
||
{
|
||
stdio: 'inherit',
|
||
env: {
|
||
...process.env,
|
||
TEST_LEVEL: level,
|
||
},
|
||
}
|
||
);
|
||
} catch (error) {
|
||
console.error(` ❌ 测试失败: ${pattern}`);
|
||
// 继续执行其他测试
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 执行全量测试
|
||
*/
|
||
async runAllTests(): Promise<void> {
|
||
console.log('=== 开始执行全量测试 ===\n');
|
||
|
||
execSync('npm run test:e2e', {
|
||
stdio: 'inherit',
|
||
});
|
||
|
||
console.log('\n✅ 全量测试执行完成');
|
||
}
|
||
}
|
||
|
||
// 主函数
|
||
async function main() {
|
||
const executor = new TestExecutor();
|
||
const testsFile = process.argv[2] || 'selected-tests.json';
|
||
|
||
if (fs.existsSync(testsFile)) {
|
||
await executor.runSelectedTests(testsFile);
|
||
} else {
|
||
await executor.runAllTests();
|
||
}
|
||
}
|
||
|
||
main().catch(console.error);
|
||
```
|
||
|
||
**Step 2: 创建全量测试执行脚本**
|
||
|
||
```typescript
|
||
// scripts/run-all-tests.ts
|
||
import { execSync } from 'child_process';
|
||
|
||
console.log('=== 开始执行全量测试 ===\n');
|
||
|
||
try {
|
||
execSync('npm run test:e2e', {
|
||
stdio: 'inherit',
|
||
});
|
||
|
||
console.log('\n✅ 全量测试执行完成');
|
||
} catch (error) {
|
||
console.error('\n❌ 全量测试执行失败');
|
||
process.exit(1);
|
||
}
|
||
```
|
||
|
||
**Step 3: 添加npm脚本**
|
||
|
||
在`package.json`中添加:
|
||
|
||
```json
|
||
{
|
||
"scripts": {
|
||
"test:smart": "ts-node scripts/run-selected-tests.ts",
|
||
"test:all": "ts-node scripts/run-all-tests.ts"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 4: 测试执行脚本**
|
||
|
||
Run: `npm run test:smart -- selected-tests.json`
|
||
|
||
Expected: 脚本运行正常(如果没有测试文件会提示)
|
||
|
||
**Step 5: 提交测试执行脚本**
|
||
|
||
```bash
|
||
git add scripts/run-selected-tests.ts scripts/run-all-tests.ts package.json
|
||
git commit -m "feat: add test execution scripts"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务1.6:集成到Woodpecker CI
|
||
|
||
**目标**:将智能测试流程集成到Woodpecker CI
|
||
|
||
**Files:**
|
||
- Modify: `.woodpecker.yml`
|
||
|
||
**Step 1: 更新Woodpecker CI配置**
|
||
|
||
在`.woodpecker.yml`中添加智能测试步骤:
|
||
|
||
```yaml
|
||
# .woodpecker.yml (追加内容)
|
||
|
||
steps:
|
||
# ... 现有步骤 ...
|
||
|
||
# 智能测试选择
|
||
smart-test-selection:
|
||
image: node:18-alpine
|
||
commands:
|
||
- npm ci
|
||
- |
|
||
# 获取变更文件
|
||
if [ "$CI_BUILD_EVENT" = "cron" ]; then
|
||
echo "[]" > changed-files.txt
|
||
else
|
||
git diff --name-only origin/main...HEAD > changed-files.txt || echo "[]" > changed-files.txt
|
||
fi
|
||
- |
|
||
# 智能选择测试用例
|
||
node scripts/cli/smart-test-selector-cli.js \
|
||
--input changed-files.txt \
|
||
--output selected-tests.json \
|
||
--report test-selection-report.md
|
||
when:
|
||
event: [push, pull_request]
|
||
|
||
# 执行智能测试
|
||
run-smart-tests:
|
||
image: node:18-alpine
|
||
environment:
|
||
- TEST_ENV=ci
|
||
- API_BASE_URL=http://localhost:8083
|
||
- FRONTEND_BASE_URL=http://localhost:5174
|
||
commands:
|
||
- npm ci
|
||
- npx playwright install --with-deps chromium
|
||
- |
|
||
if [ -f selected-tests.json ]; then
|
||
npm run test:smart selected-tests.json
|
||
else
|
||
npm run test:all
|
||
fi
|
||
depends_on:
|
||
- start-test-environment
|
||
- smart-test-selection
|
||
```
|
||
|
||
**Step 2: 验证CI配置语法**
|
||
|
||
Run: `woodpecker-cli lint .woodpecker.yml`
|
||
|
||
Expected: 配置文件语法正确
|
||
|
||
**Step 3: 提交CI配置更新**
|
||
|
||
```bash
|
||
git add .woodpecker.yml
|
||
git commit -m "feat: integrate smart test selection into Woodpecker CI"
|
||
```
|
||
|
||
---
|
||
|
||
## 阶段1完成检查点
|
||
|
||
完成以上所有任务后,您应该拥有:
|
||
|
||
- ✅ 容器化测试环境配置(docker-compose.test.yml)
|
||
- ✅ 测试数据库初始化脚本
|
||
- ✅ 代码-测试映射配置
|
||
- ✅ 智能测试选择器实现
|
||
- ✅ 测试执行脚本
|
||
- ✅ Woodpecker CI集成
|
||
|
||
**验证步骤**:
|
||
|
||
1. 启动测试环境:`docker-compose -f docker-compose.test.yml up -d`
|
||
2. 初始化测试数据库:`./scripts/init-test-database.sh`
|
||
3. 运行智能测试选择:`npm run test:smart`
|
||
4. 检查CI配置:`woodpecker-cli lint .woodpecker.yml`
|
||
|
||
---
|
||
|
||
## 阶段2:报告体系与缺陷管理(第3-4周)
|
||
|
||
### 任务2.1:实现报告生成器
|
||
|
||
**目标**:创建多层次报告生成器(实时、汇总、趋势)
|
||
|
||
**Files:**
|
||
- Create: `scripts/report-generator.ts`
|
||
- Create: `scripts/generate-trend-report.ts`
|
||
|
||
**Step 1: 创建报告生成器核心类**
|
||
|
||
```typescript
|
||
// scripts/report-generator.ts
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
|
||
export interface TestResult {
|
||
testName: string;
|
||
status: 'passed' | 'failed' | 'skipped';
|
||
duration: number;
|
||
module: string;
|
||
priority: string;
|
||
errorMessage?: string;
|
||
screenshots?: string[];
|
||
logs?: string[];
|
||
}
|
||
|
||
export interface TestReport {
|
||
timestamp: string;
|
||
totalTests: number;
|
||
passed: number;
|
||
failed: number;
|
||
skipped: number;
|
||
duration: number;
|
||
modules: {
|
||
[module: string]: {
|
||
total: number;
|
||
passed: number;
|
||
failed: number;
|
||
skipped: number;
|
||
};
|
||
};
|
||
results: TestResult[];
|
||
}
|
||
|
||
export class ReportGenerator {
|
||
private outputDir: string;
|
||
|
||
constructor(outputDir: string = './test-results') {
|
||
this.outputDir = outputDir;
|
||
this.ensureOutputDir();
|
||
}
|
||
|
||
/**
|
||
* 生成实时报告
|
||
*/
|
||
generateRealtimeReport(result: TestResult): void {
|
||
const reportPath = path.join(this.outputDir, 'realtime-report.json');
|
||
|
||
let report: TestReport;
|
||
if (fs.existsSync(reportPath)) {
|
||
report = JSON.parse(fs.readFileSync(reportPath, 'utf-8'));
|
||
} else {
|
||
report = this.createEmptyReport();
|
||
}
|
||
|
||
report.totalTests++;
|
||
report[result.status]++;
|
||
report.duration += result.duration;
|
||
|
||
if (!report.modules[result.module]) {
|
||
report.modules[result.module] = {
|
||
total: 0,
|
||
passed: 0,
|
||
failed: 0,
|
||
skipped: 0,
|
||
};
|
||
}
|
||
report.modules[result.module].total++;
|
||
report.modules[result.module][result.status]++;
|
||
|
||
report.results.push(result);
|
||
|
||
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
||
this.generateRealtimeHTML(report);
|
||
}
|
||
|
||
/**
|
||
* 生成汇总报告
|
||
*/
|
||
generateSummaryReport(): TestReport {
|
||
const reportPath = path.join(this.outputDir, 'realtime-report.json');
|
||
|
||
if (!fs.existsSync(reportPath)) {
|
||
throw new Error('No test results found');
|
||
}
|
||
|
||
const report: TestReport = JSON.parse(
|
||
fs.readFileSync(reportPath, 'utf-8')
|
||
);
|
||
|
||
this.generateHTMLReport(report);
|
||
this.generateJSONReport(report);
|
||
this.generateJUnitReport(report);
|
||
|
||
return report;
|
||
}
|
||
|
||
/**
|
||
* 生成HTML报告
|
||
*/
|
||
private generateHTMLReport(report: TestReport): void {
|
||
const html = this.createHTMLTemplate(report);
|
||
const reportPath = path.join(this.outputDir, 'reports', 'summary-report.html');
|
||
fs.writeFileSync(reportPath, html);
|
||
}
|
||
|
||
/**
|
||
* 创建HTML模板
|
||
*/
|
||
private createHTMLTemplate(report: TestReport): string {
|
||
return `
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>测试报告 - ${report.timestamp}</title>
|
||
<style>
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
margin: 0;
|
||
padding: 20px;
|
||
background-color: #f5f5f5;
|
||
}
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
background: white;
|
||
padding: 30px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
}
|
||
.summary {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 20px;
|
||
margin-bottom: 30px;
|
||
}
|
||
.summary-card {
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
text-align: center;
|
||
}
|
||
.summary-card.total { background: #e3f2fd; }
|
||
.summary-card.passed { background: #e8f5e9; }
|
||
.summary-card.failed { background: #ffebee; }
|
||
.summary-card.skipped { background: #fff3e0; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>自动化测试报告</h1>
|
||
<p>生成时间: ${report.timestamp}</p>
|
||
<p>总耗时: ${(report.duration / 1000).toFixed(2)}秒</p>
|
||
|
||
<div class="summary">
|
||
<div class="summary-card total">
|
||
<h3>总测试数</h3>
|
||
<div class="number">${report.totalTests}</div>
|
||
</div>
|
||
<div class="summary-card passed">
|
||
<h3>通过</h3>
|
||
<div class="number">${report.passed}</div>
|
||
</div>
|
||
<div class="summary-card failed">
|
||
<h3>失败</h3>
|
||
<div class="number">${report.failed}</div>
|
||
</div>
|
||
<div class="summary-card skipped">
|
||
<h3>跳过</h3>
|
||
<div class="number">${report.skipped}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`.trim();
|
||
}
|
||
|
||
/**
|
||
* 辅助方法
|
||
*/
|
||
private createEmptyReport(): TestReport {
|
||
return {
|
||
timestamp: new Date().toISOString(),
|
||
totalTests: 0,
|
||
passed: 0,
|
||
failed: 0,
|
||
skipped: 0,
|
||
duration: 0,
|
||
modules: {},
|
||
results: [],
|
||
};
|
||
}
|
||
|
||
private ensureOutputDir(): void {
|
||
const dirs = [
|
||
this.outputDir,
|
||
path.join(this.outputDir, 'reports'),
|
||
path.join(this.outputDir, 'history'),
|
||
];
|
||
|
||
dirs.forEach(dir => {
|
||
if (!fs.existsSync(dir)) {
|
||
fs.mkdirSync(dir, { recursive: true });
|
||
}
|
||
});
|
||
}
|
||
|
||
private generateJSONReport(report: TestReport): void {
|
||
const reportPath = path.join(this.outputDir, 'reports', 'summary-report.json');
|
||
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
||
}
|
||
|
||
private generateJUnitReport(report: TestReport): void {
|
||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||
<testsuites>
|
||
${Object.entries(report.modules).map(([module, stats]) => `
|
||
<testsuite name="${module}" tests="${stats.total}" failures="${stats.failed}" skipped="${stats.skipped}">
|
||
</testsuite>
|
||
`).join('')}
|
||
</testsuites>
|
||
`.trim();
|
||
|
||
const reportPath = path.join(this.outputDir, 'reports', 'junit-report.xml');
|
||
fs.writeFileSync(reportPath, xml);
|
||
}
|
||
|
||
private generateRealtimeHTML(report: TestReport): void {
|
||
const html = `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta http-equiv="refresh" content="5">
|
||
<title>实时测试报告</title>
|
||
</head>
|
||
<body>
|
||
<h1>实时测试进度</h1>
|
||
<p>已执行: ${report.totalTests}</p>
|
||
<p>通过: ${report.passed}</p>
|
||
<p>失败: ${report.failed}</p>
|
||
</body>
|
||
</html>
|
||
`.trim();
|
||
|
||
const reportPath = path.join(this.outputDir, 'reports', 'realtime-report.html');
|
||
fs.writeFileSync(reportPath, html);
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: 创建趋势报告生成脚本**
|
||
|
||
```typescript
|
||
// scripts/generate-trend-report.ts
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
import { ReportGenerator, TestReport } from './report-generator';
|
||
|
||
const reportGenerator = new ReportGenerator();
|
||
|
||
// 生成趋势报告
|
||
const historyDir = path.join('./test-results', 'history');
|
||
const currentReport = reportGenerator.generateSummaryReport();
|
||
|
||
// 保存当前报告到历史记录
|
||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||
const historyPath = path.join(historyDir, `report-${timestamp}.json`);
|
||
fs.writeFileSync(historyPath, JSON.stringify(currentReport, null, 2));
|
||
|
||
console.log('✅ 趋势报告生成完成');
|
||
```
|
||
|
||
**Step 3: 添加npm脚本**
|
||
|
||
在`package.json`中添加:
|
||
|
||
```json
|
||
{
|
||
"scripts": {
|
||
"test:report": "ts-node scripts/generate-trend-report.ts"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 4: 测试报告生成器**
|
||
|
||
Run: `npm run test:report`
|
||
|
||
Expected: 报告生成成功
|
||
|
||
**Step 5: 提交报告生成器代码**
|
||
|
||
```bash
|
||
git add scripts/report-generator.ts scripts/generate-trend-report.ts package.json
|
||
git commit -m "feat: implement multi-level report generator"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务2.2:实现企业微信智能表格集成
|
||
|
||
**目标**:实现与企业微信智能表格的集成,自动同步缺陷
|
||
|
||
**Files:**
|
||
- Create: `scripts/wecom-integration.ts`
|
||
- Create: `scripts/analyze-failures.ts`
|
||
|
||
**Step 1: 创建企业微信集成类**
|
||
|
||
```typescript
|
||
// scripts/wecom-integration.ts
|
||
import axios from 'axios';
|
||
|
||
export interface WecomTableConfig {
|
||
webhookUrl: string;
|
||
tableId: string;
|
||
}
|
||
|
||
export interface DefectRecord {
|
||
defectId: string;
|
||
testName: string;
|
||
module: string;
|
||
priority: 'P0' | 'P1' | 'P2';
|
||
status: 'open' | 'in_progress' | 'fixed' | 'closed';
|
||
errorMessage: string;
|
||
screenshots: string[];
|
||
logs: string[];
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
reporter: string;
|
||
testRunId: string;
|
||
gitCommit?: string;
|
||
gitBranch?: string;
|
||
}
|
||
|
||
export class WecomTableIntegration {
|
||
private config: WecomTableConfig;
|
||
|
||
constructor(config: WecomTableConfig) {
|
||
this.config = config;
|
||
}
|
||
|
||
/**
|
||
* 同步缺陷到企业微信智能表格
|
||
*/
|
||
async syncDefect(defect: DefectRecord): Promise<void> {
|
||
try {
|
||
const existingDefect = await this.findExistingDefect(defect.testName);
|
||
|
||
if (existingDefect) {
|
||
await this.updateDefect(existingDefect.id, defect);
|
||
console.log(`✅ 更新缺陷: ${defect.testName}`);
|
||
} else {
|
||
await this.createDefect(defect);
|
||
console.log(`✅ 创建缺陷: ${defect.testName}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ 同步缺陷失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发送通知到企业微信群
|
||
*/
|
||
async sendNotification(message: {
|
||
title: string;
|
||
content: string;
|
||
mentionedList?: string[];
|
||
}): Promise<void> {
|
||
const payload = {
|
||
msgtype: 'markdown',
|
||
markdown: {
|
||
content: `## ${message.title}\n\n${message.content}${
|
||
message.mentionedList
|
||
? `\n\n<@${message.mentionedList.join('><@')}>`
|
||
: ''
|
||
}`,
|
||
},
|
||
};
|
||
|
||
await axios.post(this.config.webhookUrl, payload);
|
||
}
|
||
|
||
private async createDefect(defect: DefectRecord): Promise<void> {
|
||
const payload = {
|
||
table_id: this.config.tableId,
|
||
record: {
|
||
fields: {
|
||
缺陷ID: defect.defectId,
|
||
测试用例: defect.testName,
|
||
模块: defect.module,
|
||
优先级: defect.priority,
|
||
状态: defect.status,
|
||
错误信息: defect.errorMessage,
|
||
创建时间: defect.createdAt,
|
||
更新时间: defect.updatedAt,
|
||
报告人: defect.reporter,
|
||
},
|
||
},
|
||
};
|
||
|
||
await this.sendToWecom(payload);
|
||
}
|
||
|
||
private async updateDefect(
|
||
defectId: string,
|
||
defect: DefectRecord
|
||
): Promise<void> {
|
||
const payload = {
|
||
table_id: this.config.tableId,
|
||
record_id: defectId,
|
||
record: {
|
||
fields: {
|
||
状态: defect.status,
|
||
更新时间: defect.updatedAt,
|
||
错误信息: defect.errorMessage,
|
||
},
|
||
},
|
||
};
|
||
|
||
await this.sendToWecom(payload);
|
||
}
|
||
|
||
private async findExistingDefect(
|
||
testName: string
|
||
): Promise<{ id: string } | null> {
|
||
// 实现查找逻辑
|
||
return null;
|
||
}
|
||
|
||
private async sendToWecom(payload: any): Promise<void> {
|
||
await axios.post(this.config.webhookUrl, payload, {
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: 创建缺陷分析脚本**
|
||
|
||
```typescript
|
||
// scripts/analyze-failures.ts
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
import { execSync } from 'child_process';
|
||
import { WecomTableIntegration, DefectRecord } from './wecom-integration';
|
||
|
||
export class FailureAnalyzer {
|
||
private wecomIntegration: WecomTableIntegration;
|
||
private testResultsDir: string;
|
||
|
||
constructor(
|
||
wecomConfig: any,
|
||
testResultsDir: string = './test-results'
|
||
) {
|
||
this.wecomIntegration = new WecomTableIntegration(wecomConfig);
|
||
this.testResultsDir = testResultsDir;
|
||
}
|
||
|
||
async analyzeAndSync(): Promise<void> {
|
||
const failures = this.loadFailures();
|
||
|
||
if (failures.length === 0) {
|
||
console.log('✅ 没有失败的测试用例');
|
||
return;
|
||
}
|
||
|
||
console.log(`📊 发现 ${failures.length} 个失败的测试用例`);
|
||
|
||
for (const failure of failures) {
|
||
const defect = this.createDefectRecord(failure);
|
||
await this.wecomIntegration.syncDefect(defect);
|
||
}
|
||
|
||
await this.sendSummaryNotification(failures);
|
||
}
|
||
|
||
private loadFailures(): any[] {
|
||
const reportPath = path.join(this.testResultsDir, 'realtime-report.json');
|
||
|
||
if (!fs.existsSync(reportPath)) {
|
||
return [];
|
||
}
|
||
|
||
const report = JSON.parse(fs.readFileSync(reportPath, 'utf-8'));
|
||
return report.results.filter((r: any) => r.status === 'failed');
|
||
}
|
||
|
||
private createDefectRecord(failure: any): DefectRecord {
|
||
const gitInfo = this.getGitInfo();
|
||
|
||
return {
|
||
defectId: `DEF-${Date.now()}`,
|
||
testName: failure.testName,
|
||
module: failure.module,
|
||
priority: this.determinePriority(failure),
|
||
status: 'open',
|
||
errorMessage: failure.errorMessage || 'Unknown error',
|
||
screenshots: failure.screenshots || [],
|
||
logs: failure.logs || [],
|
||
createdAt: new Date().toISOString(),
|
||
updatedAt: new Date().toISOString(),
|
||
reporter: '自动化测试系统',
|
||
testRunId: process.env.CI_BUILD_NUMBER || 'local',
|
||
gitCommit: gitInfo.commit,
|
||
gitBranch: gitInfo.branch,
|
||
};
|
||
}
|
||
|
||
private determinePriority(failure: any): 'P0' | 'P1' | 'P2' {
|
||
if (failure.priority === 'p0' || failure.tags?.includes('@smoke')) {
|
||
return 'P0';
|
||
}
|
||
if (failure.priority === 'p1' || failure.tags?.includes('@functional')) {
|
||
return 'P1';
|
||
}
|
||
return 'P2';
|
||
}
|
||
|
||
private getGitInfo(): { commit: string; branch: string } {
|
||
try {
|
||
const commit = execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim();
|
||
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
||
encoding: 'utf-8',
|
||
}).trim();
|
||
return { commit, branch };
|
||
} catch (error) {
|
||
return { commit: 'unknown', branch: 'unknown' };
|
||
}
|
||
}
|
||
|
||
private async sendSummaryNotification(failures: any[]): Promise<void> {
|
||
const p0Count = failures.filter(f => this.determinePriority(f) === 'P0').length;
|
||
const p1Count = failures.filter(f => this.determinePriority(f) === 'P1').length;
|
||
const p2Count = failures.filter(f => this.determinePriority(f) === 'P2').length;
|
||
|
||
await this.wecomIntegration.sendNotification({
|
||
title: '🚨 测试失败通知',
|
||
content: `
|
||
**测试执行完成,发现 ${failures.length} 个失败用例**
|
||
|
||
- P0(核心功能): ${p0Count} 个
|
||
- P1(重要功能): ${p1Count} 个
|
||
- P2(次要功能): ${p2Count} 个
|
||
|
||
**失败详情:**
|
||
${failures.slice(0, 5).map(f => `- [${f.module}] ${f.testName}`).join('\n')}
|
||
`.trim(),
|
||
mentionedList: p0Count > 0 ? ['all'] : [],
|
||
});
|
||
}
|
||
}
|
||
|
||
// 主函数
|
||
async function main() {
|
||
const wecomConfig = {
|
||
webhookUrl: process.env.WECOM_WEBHOOK_URL || '',
|
||
tableId: process.env.WECOM_TABLE_ID || '',
|
||
};
|
||
|
||
const analyzer = new FailureAnalyzer(wecomConfig);
|
||
await analyzer.analyzeAndSync();
|
||
}
|
||
|
||
main().catch(console.error);
|
||
```
|
||
|
||
**Step 3: 安装axios依赖**
|
||
|
||
Run: `npm install axios`
|
||
|
||
Expected: axios安装成功
|
||
|
||
**Step 4: 测试企业微信集成**
|
||
|
||
Run: `WECOM_WEBHOOK_URL=your_url WECOM_TABLE_ID=your_id ts-node scripts/analyze-failures.ts`
|
||
|
||
Expected: 脚本运行正常(如果没有失败用例会提示)
|
||
|
||
**Step 5: 提交企业微信集成代码**
|
||
|
||
```bash
|
||
git add scripts/wecom-integration.ts scripts/analyze-failures.ts package.json
|
||
git commit -m "feat: implement WeChat Work smart table integration"
|
||
```
|
||
|
||
---
|
||
|
||
## 阶段2完成检查点
|
||
|
||
完成以上所有任务后,您应该拥有:
|
||
|
||
- ✅ 多层次报告生成器(实时、汇总、趋势)
|
||
- ✅ 企业微信智能表格集成
|
||
- ✅ 缺陷自动同步功能
|
||
- ✅ 企业微信群通知功能
|
||
|
||
**验证步骤**:
|
||
|
||
1. 运行测试:`npm run test:smart`
|
||
2. 生成报告:`npm run test:report`
|
||
3. 检查报告文件:`ls test-results/reports/`
|
||
4. 测试企业微信通知(需要配置Webhook)
|
||
|
||
---
|
||
|
||
## 阶段3:优化与完善(第5-6周)
|
||
|
||
### 任务3.1:引入代码覆盖率分析
|
||
|
||
**目标**:使用代码覆盖率数据自动生成和维护代码-测试映射关系
|
||
|
||
**Files:**
|
||
- Create: `scripts/coverage-analyzer.ts`
|
||
- Modify: `config/test-mapping.config.ts`
|
||
|
||
**Step 1: 创建覆盖率分析器**
|
||
|
||
```typescript
|
||
// scripts/coverage-analyzer.ts
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
|
||
export interface CoverageData {
|
||
[file: string]: {
|
||
lines: { covered: number; total: number };
|
||
functions: { covered: number; total: number };
|
||
branches: { covered: number; total: number };
|
||
};
|
||
}
|
||
|
||
export class CoverageAnalyzer {
|
||
/**
|
||
* 从Playwright覆盖率报告生成映射
|
||
*/
|
||
generateMappingFromCoverage(coverageFile: string): void {
|
||
const coverage: CoverageData = JSON.parse(
|
||
fs.readFileSync(coverageFile, 'utf-8')
|
||
);
|
||
|
||
const mapping: any = {};
|
||
|
||
for (const [file, data] of Object.entries(coverage)) {
|
||
const testFile = this.inferTestFile(file);
|
||
if (testFile) {
|
||
if (!mapping[file]) {
|
||
mapping[file] = {
|
||
tests: [],
|
||
priority: this.determinePriority(data),
|
||
modules: this.extractModule(file),
|
||
};
|
||
}
|
||
mapping[file].tests.push(testFile);
|
||
}
|
||
}
|
||
|
||
// 更新test-mapping.config.ts
|
||
this.updateMappingConfig(mapping);
|
||
}
|
||
|
||
private inferTestFile(sourceFile: string): string | null {
|
||
// 根据源文件推断测试文件
|
||
const moduleName = this.extractModule(sourceFile);
|
||
if (moduleName) {
|
||
return `e2e/${moduleName}/*.spec.ts`;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private determinePriority(data: any): 'high' | 'medium' | 'low' {
|
||
const lineCoverage = data.lines.covered / data.lines.total;
|
||
if (lineCoverage > 0.8) return 'high';
|
||
if (lineCoverage > 0.5) return 'medium';
|
||
return 'low';
|
||
}
|
||
|
||
private extractModule(file: string): string[] {
|
||
const match = file.match(/src\/views\/(\w+)/);
|
||
if (match) {
|
||
const moduleName = match[1].toLowerCase().replace(/-/g, '-');
|
||
return [moduleName];
|
||
}
|
||
return [];
|
||
}
|
||
|
||
private updateMappingConfig(mapping: any): void {
|
||
const configPath = path.join(process.cwd(), 'config', 'test-mapping.config.ts');
|
||
|
||
// 读取现有配置
|
||
const existingConfig = fs.readFileSync(configPath, 'utf-8');
|
||
|
||
// 合并映射
|
||
const mergedMapping = {
|
||
...JSON.parse(existingConfig.match(/export const testMapping[^=]*=\s*([\s\S]*?);/)?.[1] || '{}'),
|
||
...mapping,
|
||
};
|
||
|
||
// 写入新配置
|
||
const newConfig = existingConfig.replace(
|
||
/export const testMapping[^=]*=\s*[\s\S]*?;/,
|
||
`export const testMapping: TestMapping = ${JSON.stringify(mergedMapping, null, 2)};`
|
||
);
|
||
|
||
fs.writeFileSync(configPath, newConfig);
|
||
console.log('✅ 测试映射配置已更新');
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: 运行覆盖率分析**
|
||
|
||
Run: `npx playwright test --coverage && ts-node scripts/coverage-analyzer.ts coverage/coverage-final.json`
|
||
|
||
Expected: 映射配置更新成功
|
||
|
||
**Step 3: 提交覆盖率分析器**
|
||
|
||
```bash
|
||
git add scripts/coverage-analyzer.ts
|
||
git commit -m "feat: add coverage analyzer for auto-generating test mapping"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务3.2:添加历史失败率分析
|
||
|
||
**目标**:分析历史测试失败率,优先执行容易失败的测试
|
||
|
||
**Files:**
|
||
- Create: `scripts/failure-rate-analyzer.ts`
|
||
- Modify: `scripts/smart-test-selector.ts`
|
||
|
||
**Step 1: 创建失败率分析器**
|
||
|
||
```typescript
|
||
// scripts/failure-rate-analyzer.ts
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
|
||
export interface FailureRateData {
|
||
[testName: string]: {
|
||
totalRuns: number;
|
||
failures: number;
|
||
failureRate: number;
|
||
lastFailure?: string;
|
||
};
|
||
}
|
||
|
||
export class FailureRateAnalyzer {
|
||
private historyDir: string;
|
||
|
||
constructor(historyDir: string = './test-results/history') {
|
||
this.historyDir = historyDir;
|
||
}
|
||
|
||
/**
|
||
* 分析历史失败率
|
||
*/
|
||
analyzeFailureRate(): FailureRateData {
|
||
const reports = this.loadHistoryReports();
|
||
const failureRateData: FailureRateData = {};
|
||
|
||
for (const report of reports) {
|
||
for (const result of report.results) {
|
||
if (!failureRateData[result.testName]) {
|
||
failureRateData[result.testName] = {
|
||
totalRuns: 0,
|
||
failures: 0,
|
||
failureRate: 0,
|
||
};
|
||
}
|
||
|
||
failureRateData[result.testName].totalRuns++;
|
||
if (result.status === 'failed') {
|
||
failureRateData[result.testName].failures++;
|
||
failureRateData[result.testName].lastFailure = report.timestamp;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 计算失败率
|
||
for (const data of Object.values(failureRateData)) {
|
||
data.failureRate = data.failures / data.totalRuns;
|
||
}
|
||
|
||
return failureRateData;
|
||
}
|
||
|
||
/**
|
||
* 获取高风险测试(失败率 > 20%)
|
||
*/
|
||
getHighRiskTests(): string[] {
|
||
const failureRateData = this.analyzeFailureRate();
|
||
return Object.entries(failureRateData)
|
||
.filter(([_, data]) => data.failureRate > 0.2)
|
||
.map(([testName, _]) => testName);
|
||
}
|
||
|
||
private loadHistoryReports(): any[] {
|
||
if (!fs.existsSync(this.historyDir)) {
|
||
return [];
|
||
}
|
||
|
||
return fs.readdirSync(this.historyDir)
|
||
.filter(f => f.endsWith('.json'))
|
||
.map(f => JSON.parse(fs.readFileSync(path.join(this.historyDir, f), 'utf-8')))
|
||
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: 集成到智能测试选择器**
|
||
|
||
在`scripts/smart-test-selector.ts`中添加:
|
||
|
||
```typescript
|
||
import { FailureRateAnalyzer } from './failure-rate-analyzer';
|
||
|
||
export class SmartTestSelector {
|
||
private failureRateAnalyzer: FailureRateAnalyzer;
|
||
|
||
constructor() {
|
||
this.failureRateAnalyzer = new FailureRateAnalyzer();
|
||
}
|
||
|
||
/**
|
||
* 优先选择高风险测试
|
||
*/
|
||
prioritizeHighRiskTests(tests: string[]): string[] {
|
||
const highRiskTests = this.failureRateAnalyzer.getHighRiskTests();
|
||
|
||
// 将高风险测试放在前面
|
||
return [
|
||
...tests.filter(t => highRiskTests.includes(t)),
|
||
...tests.filter(t => !highRiskTests.includes(t)),
|
||
];
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 3: 提交失败率分析器**
|
||
|
||
```bash
|
||
git add scripts/failure-rate-analyzer.ts scripts/smart-test-selector.ts
|
||
git commit -m "feat: add failure rate analyzer for test prioritization"
|
||
```
|
||
|
||
---
|
||
|
||
## 阶段3完成检查点
|
||
|
||
完成以上所有任务后,您应该拥有:
|
||
|
||
- ✅ 代码覆盖率分析器
|
||
- ✅ 自动生成测试映射功能
|
||
- ✅ 历史失败率分析
|
||
- ✅ 高风险测试优先执行
|
||
|
||
**验证步骤**:
|
||
|
||
1. 运行覆盖率分析:`npm run test:coverage`
|
||
2. 检查映射更新:`git diff config/test-mapping.config.ts`
|
||
3. 查看失败率分析:`ts-node -e "import { FailureRateAnalyzer } from './scripts/failure-rate-analyzer'; const analyzer = new FailureRateAnalyzer(); console.log(analyzer.analyzeFailureRate())"`
|
||
|
||
---
|
||
|
||
## 阶段4:持续优化(长期)
|
||
|
||
### 任务4.1:完善文档和培训材料
|
||
|
||
**目标**:创建完整的使用文档和培训材料
|
||
|
||
**Files:**
|
||
- Create: `docs/automated-testing-framework-guide.md`
|
||
- Create: `docs/training-materials.md`
|
||
|
||
**Step 1: 创建使用指南**
|
||
|
||
```markdown
|
||
# 自动化测试流程框架使用指南
|
||
|
||
## 快速开始
|
||
|
||
### 1. 启动测试环境
|
||
|
||
\`\`\`bash
|
||
docker-compose -f docker-compose.test.yml up -d
|
||
\`\`\`
|
||
|
||
### 2. 初始化测试数据库
|
||
|
||
\`\`\`bash
|
||
./scripts/init-test-database.sh
|
||
\`\`\`
|
||
|
||
### 3. 运行智能测试
|
||
|
||
\`\`\`bash
|
||
npm run test:smart
|
||
\`\`\`
|
||
|
||
### 4. 查看测试报告
|
||
|
||
\`\`\`bash
|
||
open test-results/reports/summary-report.html
|
||
\`\`\`
|
||
|
||
## 常见问题
|
||
|
||
### Q: 如何添加新的测试用例?
|
||
|
||
A: 在对应的模块目录下创建新的.spec.ts文件,并添加相应的标签。
|
||
|
||
### Q: 如何更新测试映射?
|
||
|
||
A: 运行覆盖率分析:`npm run test:coverage`
|
||
|
||
### Q: 如何配置企业微信通知?
|
||
|
||
A: 在.env.test文件中配置WECOM_WEBHOOK_URL和WECOM_TABLE_ID。
|
||
```
|
||
|
||
**Step 2: 创建培训材料**
|
||
|
||
```markdown
|
||
# 自动化测试流程框架培训材料
|
||
|
||
## 培训目标
|
||
|
||
1. 理解自动化测试流程框架的设计理念
|
||
2. 掌握智能测试选择器的使用方法
|
||
3. 学会编写符合规范的测试用例
|
||
4. 了解测试报告和缺陷管理流程
|
||
|
||
## 培训内容
|
||
|
||
### 第一部分:框架概述(30分钟)
|
||
|
||
- 设计目标和核心价值
|
||
- 架构设计和技术栈
|
||
- 工作流程介绍
|
||
|
||
### 第二部分:实践操作(60分钟)
|
||
|
||
- 启动测试环境
|
||
- 编写测试用例
|
||
- 运行智能测试
|
||
- 查看测试报告
|
||
|
||
### 第三部分:最佳实践(30分钟)
|
||
|
||
- 测试用例设计规范
|
||
- 标签使用指南
|
||
- 常见问题解决
|
||
|
||
## 培训考核
|
||
|
||
- 完成一个完整的测试用例编写
|
||
- 成功运行智能测试
|
||
- 理解测试报告内容
|
||
```
|
||
|
||
**Step 3: 提交文档**
|
||
|
||
```bash
|
||
git add docs/automated-testing-framework-guide.md docs/training-materials.md
|
||
git commit -m "docs: add comprehensive guide and training materials"
|
||
```
|
||
|
||
---
|
||
|
||
## 总结
|
||
|
||
本实施计划详细描述了自动化测试流程框架的完整实施过程,包括:
|
||
|
||
### 实施内容
|
||
|
||
1. **阶段1**(第1-2周):基础框架搭建
|
||
- 容器化测试环境
|
||
- 测试数据库配置
|
||
- 智能测试选择器
|
||
- CI/CD集成
|
||
|
||
2. **阶段2**(第3-4周):报告体系与缺陷管理
|
||
- 多层次报告生成器
|
||
- 企业微信智能表格集成
|
||
- 缺陷自动同步
|
||
|
||
3. **阶段3**(第5-6周):优化与完善
|
||
- 代码覆盖率分析
|
||
- 历史失败率分析
|
||
- 测试优先级优化
|
||
|
||
4. **阶段4**(长期):持续优化
|
||
- 文档完善
|
||
- 培训材料
|
||
- 持续改进
|
||
|
||
### 预期成果
|
||
|
||
完成本实施计划后,您将拥有:
|
||
|
||
- ✅ 完整的容器化测试环境
|
||
- ✅ 智能测试选择和执行系统
|
||
- ✅ 多层次测试报告体系
|
||
- ✅ 企业微信缺陷管理集成
|
||
- ✅ 完善的文档和培训材料
|
||
|
||
### 下一步行动
|
||
|
||
**立即开始实施**:
|
||
|
||
1. 确认实施环境和权限
|
||
2. 按照阶段1的任务清单逐步执行
|
||
3. 每完成一个任务,进行验证和提交
|
||
4. 遇到问题及时反馈和调整
|
||
|
||
**需要帮助?**
|
||
|
||
如果在实施过程中遇到任何问题,请参考:
|
||
- 设计文档:`docs/plans/2026-03-28-automated-testing-framework-design.md`
|
||
- 使用指南:`docs/automated-testing-framework-guide.md`
|
||
- 培训材料:`docs/training-materials.md`
|
||
|
||
---
|
||
|
||
**实施计划版本**: v1.0
|
||
**创建日期**: 2026-03-28
|
||
**创建者**: 张翔(全栈质量保障与效能工程师)
|