添加用户管理视图、API和状态管理文件
50 KiB
自动化测试流程框架实施计划
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文件
# 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
# 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: 创建测试环境变量文件
# .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: 提交配置文件
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: 创建数据库初始化脚本
#!/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: 创建测试数据初始化脚本
// 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: 提交数据库初始化脚本
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: 创建映射配置文件
// 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: 提交映射配置文件
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: 创建智能测试选择器核心类
// 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工具
// 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: 提交智能测试选择器代码
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: 创建智能测试执行脚本
// 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: 创建全量测试执行脚本
// 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中添加:
{
"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: 提交测试执行脚本
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中添加智能测试步骤:
# .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配置更新
git add .woodpecker.yml
git commit -m "feat: integrate smart test selection into Woodpecker CI"
阶段1完成检查点
完成以上所有任务后,您应该拥有:
- ✅ 容器化测试环境配置(docker-compose.test.yml)
- ✅ 测试数据库初始化脚本
- ✅ 代码-测试映射配置
- ✅ 智能测试选择器实现
- ✅ 测试执行脚本
- ✅ Woodpecker CI集成
验证步骤:
- 启动测试环境:
docker-compose -f docker-compose.test.yml up -d - 初始化测试数据库:
./scripts/init-test-database.sh - 运行智能测试选择:
npm run test:smart - 检查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: 创建报告生成器核心类
// 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: 创建趋势报告生成脚本
// 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中添加:
{
"scripts": {
"test:report": "ts-node scripts/generate-trend-report.ts"
}
}
Step 4: 测试报告生成器
Run: npm run test:report
Expected: 报告生成成功
Step 5: 提交报告生成器代码
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: 创建企业微信集成类
// 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: 创建缺陷分析脚本
// 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: 提交企业微信集成代码
git add scripts/wecom-integration.ts scripts/analyze-failures.ts package.json
git commit -m "feat: implement WeChat Work smart table integration"
阶段2完成检查点
完成以上所有任务后,您应该拥有:
- ✅ 多层次报告生成器(实时、汇总、趋势)
- ✅ 企业微信智能表格集成
- ✅ 缺陷自动同步功能
- ✅ 企业微信群通知功能
验证步骤:
- 运行测试:
npm run test:smart - 生成报告:
npm run test:report - 检查报告文件:
ls test-results/reports/ - 测试企业微信通知(需要配置Webhook)
阶段3:优化与完善(第5-6周)
任务3.1:引入代码覆盖率分析
目标:使用代码覆盖率数据自动生成和维护代码-测试映射关系
Files:
- Create:
scripts/coverage-analyzer.ts - Modify:
config/test-mapping.config.ts
Step 1: 创建覆盖率分析器
// 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: 提交覆盖率分析器
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: 创建失败率分析器
// 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中添加:
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: 提交失败率分析器
git add scripts/failure-rate-analyzer.ts scripts/smart-test-selector.ts
git commit -m "feat: add failure rate analyzer for test prioritization"
阶段3完成检查点
完成以上所有任务后,您应该拥有:
- ✅ 代码覆盖率分析器
- ✅ 自动生成测试映射功能
- ✅ 历史失败率分析
- ✅ 高风险测试优先执行
验证步骤:
- 运行覆盖率分析:
npm run test:coverage - 检查映射更新:
git diff config/test-mapping.config.ts - 查看失败率分析:
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: 创建使用指南
# 自动化测试流程框架使用指南
## 快速开始
### 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: 创建培训材料
# 自动化测试流程框架培训材料
## 培训目标
1. 理解自动化测试流程框架的设计理念
2. 掌握智能测试选择器的使用方法
3. 学会编写符合规范的测试用例
4. 了解测试报告和缺陷管理流程
## 培训内容
### 第一部分:框架概述(30分钟)
- 设计目标和核心价值
- 架构设计和技术栈
- 工作流程介绍
### 第二部分:实践操作(60分钟)
- 启动测试环境
- 编写测试用例
- 运行智能测试
- 查看测试报告
### 第三部分:最佳实践(30分钟)
- 测试用例设计规范
- 标签使用指南
- 常见问题解决
## 培训考核
- 完成一个完整的测试用例编写
- 成功运行智能测试
- 理解测试报告内容
Step 3: 提交文档
git add docs/automated-testing-framework-guide.md docs/training-materials.md
git commit -m "docs: add comprehensive guide and training materials"
总结
本实施计划详细描述了自动化测试流程框架的完整实施过程,包括:
实施内容
-
阶段1(第1-2周):基础框架搭建
- 容器化测试环境
- 测试数据库配置
- 智能测试选择器
- CI/CD集成
-
阶段2(第3-4周):报告体系与缺陷管理
- 多层次报告生成器
- 企业微信智能表格集成
- 缺陷自动同步
-
阶段3(第5-6周):优化与完善
- 代码覆盖率分析
- 历史失败率分析
- 测试优先级优化
-
阶段4(长期):持续优化
- 文档完善
- 培训材料
- 持续改进
预期成果
完成本实施计划后,您将拥有:
- ✅ 完整的容器化测试环境
- ✅ 智能测试选择和执行系统
- ✅ 多层次测试报告体系
- ✅ 企业微信缺陷管理集成
- ✅ 完善的文档和培训材料
下一步行动
立即开始实施:
- 确认实施环境和权限
- 按照阶段1的任务清单逐步执行
- 每完成一个任务,进行验证和提交
- 遇到问题及时反馈和调整
需要帮助?
如果在实施过程中遇到任何问题,请参考:
- 设计文档:
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
创建者: 张翔(全栈质量保障与效能工程师)