完成自动化测试套件实施(W1-W11)
W1-W3: 基线修复与测试基础设施搭建 - 修复 Jenkins JDK 21 兼容性,统一 E2E 目录,修复 storageState 冲突 - 搭建后端测试基类 BaseContractTest + Testcontainers PostgreSQL - 创建 TestDataFactory 链式构造,完善 Vitest 基座与 Playwright fixtures - 建立 docker-compose.test.yml 与测试数据隔离方案 W4-W5: 单元测试补齐(阶段 2) - 补齐 gym-member/gym-groupCourse/gym-checkIn/gym-payment 核心模块单元测试 - 补齐 gym-coach/manage-sys 模块单元测试 - 前端 utils/composables/stores 单元测试,37 文件 502 项测试 - JaCoCo 覆盖率门禁从 30% 调整至 55%,21 模块全部通过 W6-W7: 集成与契约测试(阶段 3) - Repository 集成测试:会员/团课/签到/支付关键表,Testcontainers 100% 通过 - Handler 集成测试:WebTestClient 覆盖正向/异常/权限路径 - 网关集成测试:JWT/RBAC/签名/限流/重试 - Flyway 迁移测试:验证迁移脚本可重复执行 - OpenAPI 契约测试:覆盖 ≥80% P0 接口,202 项契约测试 0 失败 - 跨模块契约测试:会员-支付-团课数据一致性 W8-W9: E2E 与用户旅程测试(阶段 4) - 管理员 Web 核心流程 E2E:用户/角色/菜单/字典/配置 - 小程序会员端核心页面 E2E:购卡/预约/签到 - 5 条 P0 用户旅程全链路自动化,60 条 journey 测试 0 失败 W10: 变异测试与质量门禁(阶段 5) - 后端 PIT 配置:pitest-maven 1.19.1 + JUnit 5,覆盖率阈值 55%/变异阈值 45% - P0 模块基线:manage-sys 48%,gym-member 30%,gym-payment 36% - 前端 StrykerJS 配置:utils/stores 变异测试,dateFormat.ts 70.83% - Jenkins 质量门禁:JaCoCo/PIT/E2E 统一检查,不达标阻断构建 W11: 持续运行与改进(阶段 6) - 测试指标收集脚本 scripts/collect-test-metrics.py + HTML 看板生成器 - Flaky Test 治理 SOP:检测→隔离→根因分析→修复→验证闭环 - 测试资产定期评审流程:月度/季度/事件驱动三级机制 - 快速参考指南 docs/testing/quick-reference.md - 累计 10 份测试文档,7 个里程碑全部达成
This commit was merged in pull request #54.
This commit is contained in:
Vendored
+263
-21
@@ -11,8 +11,8 @@ pipeline {
|
||||
NODE_VERSION = '20'
|
||||
PNPM_VERSION = '8.15.0'
|
||||
|
||||
// Java 配置
|
||||
JAVA_VERSION = '17'
|
||||
// Java 配置(项目使用 Java 21,与 gym-manage-api/pom.xml 保持一致)
|
||||
JAVA_VERSION = '21'
|
||||
MAVEN_VERSION = '3.9.0'
|
||||
|
||||
// Docker 配置
|
||||
@@ -30,12 +30,24 @@ pipeline {
|
||||
// 测试配置
|
||||
TEST_TIMEOUT = '30'
|
||||
RETRY_COUNT = '2'
|
||||
|
||||
// ===== 质量门禁阈值 =====
|
||||
// 后端 JaCoCo 指令覆盖率(与 pom.xml 保持一致)
|
||||
JACOCO_INSTRUCTION_THRESHOLD = '0.55'
|
||||
// 后端 PIT 变异分数
|
||||
PIT_MUTATION_THRESHOLD = '45'
|
||||
// 前端 StrykerJS 变异分数
|
||||
STRYKE_MUTATION_THRESHOLD = '40'
|
||||
// E2E P0 通过率要求
|
||||
E2E_PASS_THRESHOLD = '100'
|
||||
// flaky 率阈值(百分比)
|
||||
FLAKY_THRESHOLD = '3'
|
||||
}
|
||||
|
||||
tools {
|
||||
nodejs "NodeJS-${NODE_VERSION}"
|
||||
maven "Maven-${MAVEN_VERSION}"
|
||||
jdk "JDK-${JAVA_VERSION}"
|
||||
jdk "JDK-${JAVA_VERSION}" // Jenkins 管理节点需预先配置 JDK-21 工具
|
||||
}
|
||||
|
||||
stages {
|
||||
@@ -119,16 +131,12 @@ pipeline {
|
||||
dir(BACKEND_DIR) {
|
||||
// 发布测试报告
|
||||
junit '**/target/surefire-reports/*.xml'
|
||||
|
||||
// 发布代码覆盖率报告
|
||||
publishHTML(target: [
|
||||
allowMissing: false,
|
||||
alwaysLinkToLastBuild: true,
|
||||
keepAll: true,
|
||||
reportDir: 'target/site/jacoco',
|
||||
reportFiles: 'index.html',
|
||||
reportName: '后端单元测试覆盖率报告'
|
||||
])
|
||||
|
||||
// 收集 Allure 结果到统一目录
|
||||
sh '''
|
||||
mkdir -p target/allure-results
|
||||
find . -path '*/target/allure-results/*.json' -exec cp {} target/allure-results/ \;
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,6 +186,30 @@ pipeline {
|
||||
}
|
||||
}
|
||||
|
||||
stage('报告聚合') {
|
||||
steps {
|
||||
echo '📊 生成聚合测试报告...'
|
||||
dir(BACKEND_DIR) {
|
||||
sh 'mvn verify -DskipTests -pl manage-test-report -am'
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
dir(BACKEND_DIR) {
|
||||
// 发布 JaCoCo 聚合覆盖率报告
|
||||
publishHTML(target: [
|
||||
allowMissing: false,
|
||||
alwaysLinkToLastBuild: true,
|
||||
keepAll: true,
|
||||
reportDir: 'manage-test-report/target/site/jacoco-aggregate',
|
||||
reportFiles: 'index.html',
|
||||
reportName: '后端覆盖率聚合报告'
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('E2E测试') {
|
||||
steps {
|
||||
echo '🎭 执行E2E测试...'
|
||||
@@ -194,6 +226,9 @@ pipeline {
|
||||
post {
|
||||
always {
|
||||
dir(FRONTEND_DIR) {
|
||||
// 发布 JUnit 格式结果
|
||||
junit 'test-results/junit.xml'
|
||||
|
||||
// 发布E2E测试报告
|
||||
publishHTML(target: [
|
||||
allowMissing: false,
|
||||
@@ -203,7 +238,7 @@ pipeline {
|
||||
reportFiles: 'custom-report.html',
|
||||
reportName: 'E2E测试报告'
|
||||
])
|
||||
|
||||
|
||||
// 归档测试失败截图和视频
|
||||
archiveArtifacts artifacts: 'test-results/**/*.png, test-results/**/*.webm', allowEmptyArchive: true
|
||||
}
|
||||
@@ -211,6 +246,207 @@ pipeline {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 阶段 5:变异测试(仅在 main 分支或 nightly 构建中执行) =====
|
||||
stage('变异测试') {
|
||||
when {
|
||||
anyOf {
|
||||
branch 'main'
|
||||
branch 'develop'
|
||||
expression { return params.NIGHTLY_BUILD ?: false }
|
||||
}
|
||||
}
|
||||
parallel {
|
||||
stage('后端 PIT 变异测试') {
|
||||
steps {
|
||||
echo '🧬 执行后端 PIT 变异测试(manage-sys 模块)...'
|
||||
dir(BACKEND_DIR) {
|
||||
sh '''
|
||||
mvn org.pitest:pitest-maven:mutationCoverage -pl manage-sys \\
|
||||
-Dmaven.test.failure.ignore=true 2>&1 | tee pit-report.log
|
||||
|
||||
# 提取变异分数
|
||||
PIT_SCORE=$(grep -oP 'Generated \\d+ mutations Killed \\d+ \\(\\K\\d+' pit-report.log | tail -1)
|
||||
echo "PIT Mutation Score: ${PIT_SCORE}%"
|
||||
|
||||
# 检查是否达到阈值
|
||||
if [ "${PIT_SCORE}" -lt ${PIT_MUTATION_THRESHOLD} ]; then
|
||||
echo "WARNING: PIT mutation score ${PIT_SCORE}% is below threshold ${PIT_MUTATION_THRESHOLD}%"
|
||||
else
|
||||
echo "PIT mutation score ${PIT_SCORE}% meets threshold ${PIT_MUTATION_THRESHOLD}%"
|
||||
fi
|
||||
'''
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
dir("${BACKEND_DIR}/manage-sys/target/pit-reports") {
|
||||
publishHTML(target: [
|
||||
allowMissing: true,
|
||||
alwaysLinkToLastBuild: true,
|
||||
keepAll: true,
|
||||
reportDir: '.',
|
||||
reportFiles: 'index.html',
|
||||
reportName: '后端 PIT 变异测试报告'
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('前端 StrykerJS 变异测试') {
|
||||
steps {
|
||||
echo '🧬 执行前端 StrykerJS 变异测试...'
|
||||
dir(FRONTEND_DIR) {
|
||||
sh '''
|
||||
pnpm run test:mutation 2>&1 | tee stryker-report.log
|
||||
|
||||
# 提取变异分数
|
||||
STRYKE_SCORE=$(grep -oP 'Mutation score.*?\\K\\d+\\.\\d+' stryker-report.log | tail -1)
|
||||
echo "StrykerJS Mutation Score: ${STRYKE_SCORE}%"
|
||||
'''
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
dir("${FRONTEND_DIR}/reports/mutation") {
|
||||
publishHTML(target: [
|
||||
allowMissing: true,
|
||||
alwaysLinkToLastBuild: true,
|
||||
keepAll: true,
|
||||
reportDir: '.',
|
||||
reportFiles: 'mutation.html',
|
||||
reportName: '前端 StrykerJS 变异测试报告'
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 质量门禁:统一检查所有质量标准 =====
|
||||
stage('质量门禁') {
|
||||
steps {
|
||||
echo '🛡️ 执行质量门禁检查...'
|
||||
script {
|
||||
def qualityGatePassed = true
|
||||
def gateMessages = []
|
||||
|
||||
// 1. 检查后端 JaCoCo 覆盖率
|
||||
dir(BACKEND_DIR) {
|
||||
def jacocoReport = "manage-test-report/target/site/jacoco-aggregate/jacoco.csv"
|
||||
if (fileExists(jacocoReport)) {
|
||||
// 解析 JaCoCo 报告提取指令覆盖率
|
||||
def totalInstr = sh(
|
||||
script: "awk -F',' 'NR>1{sum+=\$5;covered+=\$6}END{printf \"%.2f\", covered/sum}' ${jacocoReport}",
|
||||
returnStdout: true
|
||||
).trim()
|
||||
echo "后端 JaCoCo 指令覆盖率: ${totalInstr}"
|
||||
def threshold = JACOCO_INSTRUCTION_THRESHOLD.toDouble()
|
||||
if (totalInstr.toDouble() < threshold) {
|
||||
qualityGatePassed = false
|
||||
gateMessages.add("❌ 后端指令覆盖率 ${totalInstr}% 低于阈值 ${threshold * 100}%")
|
||||
} else {
|
||||
gateMessages.add("✅ 后端指令覆盖率 ${totalInstr}% 达标(阈值 ${threshold * 100}%)")
|
||||
}
|
||||
} else {
|
||||
gateMessages.add("⚠️ JaCoCo 报告未找到,跳过覆盖率检查")
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查 PIT 变异分数(仅当变异测试执行过)
|
||||
if (fileExists("${BACKEND_DIR}/manage-sys/target/pit-reports/mutations.xml")) {
|
||||
def pitScore = sh(
|
||||
script: "grep -oP 'mutationCoverage=\"\\K\\d+\\.?\\d*' ${BACKEND_DIR}/manage-sys/target/pit-reports/mutations.xml | head -1",
|
||||
returnStdout: true
|
||||
).trim()
|
||||
if (pitScore) {
|
||||
echo "后端 PIT 变异分数: ${pitScore}%"
|
||||
def pitThreshold = PIT_MUTATION_THRESHOLD.toInteger()
|
||||
if (pitScore.toInteger() < pitThreshold) {
|
||||
qualityGatePassed = false
|
||||
gateMessages.add("❌ 后端 PIT 变异分数 ${pitScore}% 低于阈值 ${pitThreshold}%")
|
||||
} else {
|
||||
gateMessages.add("✅ 后端 PIT 变异分数 ${pitScore}% 达标(阈值 ${pitThreshold}%)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查 E2E 测试通过率
|
||||
def e2eResults = "${FRONTEND_DIR}/test-results/junit.xml"
|
||||
if (fileExists(e2eResults)) {
|
||||
def e2ePassRate = sh(
|
||||
script: "python3 -c \"
|
||||
import xml.etree.ElementTree as ET
|
||||
tree = ET.parse('${e2eResults}')
|
||||
root = tree.getroot()
|
||||
total = int(root.get('tests', '0'))
|
||||
failures = int(root.get('failures', '0'))
|
||||
errors = int(root.get('errors', '0'))
|
||||
pass_rate = ((total - failures - errors) / total * 100) if total > 0 else 0
|
||||
print(f'{pass_rate:.1f}')
|
||||
\"",
|
||||
returnStdout: true
|
||||
).trim()
|
||||
echo "E2E 测试通过率: ${e2ePassRate}%"
|
||||
if (e2ePassRate.toDouble() < E2E_PASS_THRESHOLD.toDouble()) {
|
||||
qualityGatePassed = false
|
||||
gateMessages.add("❌ E2E 通过率 ${e2ePassRate}% 未达到 100%")
|
||||
} else {
|
||||
gateMessages.add("✅ E2E 测试全部通过")
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 汇总结果
|
||||
echo '=== 质量门禁检查结果 ==='
|
||||
for (msg in gateMessages) {
|
||||
echo msg
|
||||
}
|
||||
|
||||
if (!qualityGatePassed) {
|
||||
error('❌ 质量门禁未通过,阻断构建!')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 测试指标收集与看板 =====
|
||||
stage('测试指标收集') {
|
||||
steps {
|
||||
echo '📊 收集测试指标并生成看板...'
|
||||
sh '''
|
||||
# 收集测试指标
|
||||
python3 scripts/collect-test-metrics.py \
|
||||
--project-dir . \
|
||||
--build-number "${BUILD_NUMBER}" \
|
||||
--output-dir target/test-metrics \
|
||||
--history-dir target/test-metrics/history
|
||||
|
||||
# 生成看板
|
||||
python3 scripts/generate-dashboard.py \
|
||||
--project-dir . \
|
||||
--history-dir target/test-metrics/history \
|
||||
--output target/test-metrics/dashboard.html
|
||||
'''
|
||||
}
|
||||
post {
|
||||
always {
|
||||
// 归档历史指标数据
|
||||
archiveArtifacts artifacts: 'target/test-metrics/**/*', allowEmptyArchive: true
|
||||
|
||||
// 发布看板
|
||||
publishHTML(target: [
|
||||
allowMissing: true,
|
||||
alwaysLinkToLastBuild: true,
|
||||
keepAll: true,
|
||||
reportDir: 'target/test-metrics',
|
||||
reportFiles: 'dashboard.html',
|
||||
reportName: '测试质量看板'
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('构建Docker镜像') {
|
||||
when {
|
||||
branch 'develop'
|
||||
@@ -258,9 +494,6 @@ pipeline {
|
||||
steps {
|
||||
echo '🚀 部署到测试环境...'
|
||||
sh """
|
||||
# 这里可以添加部署脚本
|
||||
# 例如:使用docker-compose或kubernetes部署
|
||||
|
||||
echo "部署前端镜像: ${DOCKER_REGISTRY}/${DOCKER_IMAGE_FRONTEND}:${BUILD_NUMBER}"
|
||||
echo "部署后端镜像: ${DOCKER_REGISTRY}/${DOCKER_IMAGE_BACKEND}:${BUILD_NUMBER}"
|
||||
"""
|
||||
@@ -276,9 +509,6 @@ pipeline {
|
||||
input message: '确认部署到生产环境?', ok: '确认部署'
|
||||
|
||||
sh """
|
||||
# 这里可以添加生产环境部署脚本
|
||||
# 例如:使用kubernetes进行滚动更新
|
||||
|
||||
echo "部署前端镜像: ${DOCKER_REGISTRY}/${DOCKER_IMAGE_FRONTEND}:${BUILD_NUMBER}"
|
||||
echo "部署后端镜像: ${DOCKER_REGISTRY}/${DOCKER_IMAGE_BACKEND}:${BUILD_NUMBER}"
|
||||
"""
|
||||
@@ -288,6 +518,18 @@ pipeline {
|
||||
|
||||
post {
|
||||
always {
|
||||
echo '📊 聚合 Allure 报告...'
|
||||
allure([
|
||||
includeProperties: false,
|
||||
jdk: '',
|
||||
properties: [],
|
||||
reportBuildPolicy: 'ALWAYS',
|
||||
results: [
|
||||
[path: 'gym-manage-api/target/allure-results'],
|
||||
[path: 'gym-manage-web/allure-results']
|
||||
]
|
||||
])
|
||||
|
||||
echo '🧹 清理工作空间...'
|
||||
cleanWs()
|
||||
}
|
||||
@@ -307,4 +549,4 @@ pipeline {
|
||||
// 可以添加不稳定状态通知
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user