完成自动化测试套件实施(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:
2026-08-02 08:28:37 +08:00
parent dc68581c5e
commit 015cb0dc78
119 changed files with 21873 additions and 553 deletions
+4 -2
View File
@@ -1,9 +1,11 @@
const automator = require('miniprogram-automator');
const path = require('path');
const { resolveWechatDevtoolsCli } = require('./cli-resolver');
const PROJECT_PATH = path.resolve(__dirname, '../../unpackage/dist/dev/mp-weixin');
const CLI_PATH = 'D:\\wechat-dev-tools\\cli.bat';
const DEVTOOL_PORT = 45869;
const CLI_PATH = resolveWechatDevtoolsCli() || 'D:\\wechat-dev-tools\\cli.bat';
const DEVTOOL_PORT = process.env.WECHAT_DEVTOOLS_PORT ? Number(process.env.WECHAT_DEVTOOLS_PORT) : 45869;
/**
* 启动微信小程序
@@ -0,0 +1,45 @@
const os = require('os');
const path = require('path');
const fs = require('fs');
/**
* 解析微信开发者工具 CLI 路径。
*
* <p>优先级:
* <ol>
* <li>环境变量 {@code WECHAT_DEVTOOLS_CLI}</li>
* <li>macOS 默认安装路径</li>
* <li>Windows 默认安装路径</li>
* </ol>
* 解析不到时返回 {@code null},由调用方决定是抛出错误还是降级处理。</p>
*/
function resolveWechatDevtoolsCli() {
const envPath = process.env.WECHAT_DEVTOOLS_CLI;
if (envPath && fs.existsSync(envPath)) {
return envPath;
}
const platform = os.platform();
const candidates = [];
if (platform === 'darwin') {
candidates.push('/Applications/wechatwebdevtools.app/Contents/MacOS/cli');
candidates.push('/Applications/微信开发者工具.app/Contents/MacOS/cli');
} else if (platform === 'win32') {
candidates.push('C:\\Program Files (x86)\\Tencent\\微信web开发者工具\\cli.bat');
candidates.push('C:\\Program Files\\Tencent\\微信web开发者工具\\cli.bat');
candidates.push('D:\\wechat-dev-tools\\cli.bat');
}
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
return null;
}
module.exports = {
resolveWechatDevtoolsCli,
};
+13
View File
@@ -0,0 +1,13 @@
module.exports = {
testEnvironment: 'node',
testMatch: ['**/utils/**/*.test.js', '**/common/**/*.test.js'],
testTimeout: 30000,
verbose: true,
reporters: [
'default',
['jest-junit', {
outputDirectory: '<rootDir>/test-results',
outputName: 'junit-unit.xml',
}],
],
};
+17 -1
View File
@@ -5,5 +5,21 @@ module.exports = {
globalSetup: '<rootDir>/e2e/global-setup.js',
globalTeardown: '<rootDir>/e2e/global-teardown.js',
verbose: true,
maxWorkers: 1
maxWorkers: 1,
reporters: [
'default',
['jest-junit', {
outputDirectory: '<rootDir>/test-results',
outputName: 'junit.xml',
suiteNameTemplate: '{filepath}',
classNameTemplate: '{classname}',
titleTemplate: '{title}',
}],
],
globals: {
'process.env': {
...process.env,
NODE_ENV: 'test',
},
},
};
+48 -8
View File
@@ -15,6 +15,7 @@
},
"devDependencies": {
"jest": "^29.0.0",
"jest-junit": "^16.0.0",
"miniprogram-automator": "^0.12.1"
}
},
@@ -49,7 +50,6 @@
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
@@ -887,7 +887,6 @@
"integrity": "sha512-FrYlzZRVXP2vuVwd7Nc2dlK+iZk4g6IaT1Ib8Z6vU5Kkwlt83FJIPJ2UUFABf3bF5big0wkk8ZUihWxE4Nzdng==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/core": "^0.6.8",
"core-js": "^2.5.7"
@@ -929,7 +928,6 @@
"integrity": "sha512-7Tl6YpKTSpvwQbnGNhsfX2zyl3jRVVopd276Y2hF2zpDz9Bycow7NdfNU/4Nx1jaf96X6uWOtSVINcQ7rGd47w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.6.8",
"core-js": "^2.5.7"
@@ -1007,7 +1005,6 @@
"integrity": "sha512-CbrcpWE2xxPK1n/JoTXzhRUhP4mO07mTWaSavenCg664oQl/9XCtL+A0FekuNHzIvn4myEqvkiTwN7FsbunS/Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.6.8",
"core-js": "^2.5.7"
@@ -1137,7 +1134,6 @@
"integrity": "sha512-27nPh8L1YWsxtfmV/+Ub5dOTpXyC0HMF2cu52RQSCYxr+Lm1+23dJF70AF1poUbUe+FWXphwuUxQzjBJza9UoA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.6.8",
"core-js": "^2.5.7"
@@ -1152,7 +1148,6 @@
"integrity": "sha512-GbjETvL05BDoLdszNUV4Y0yLkHf177MnqGqilA113LIvx9aD0FtUopGXYfRGVvmtTOTouoaGJUc+K6qngvKxww==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.6.8",
"core-js": "^2.5.7"
@@ -1170,7 +1165,6 @@
"integrity": "sha512-GzIYWR/oCUK2jAwku23zt19V1ssaEU4pL0x2XsLNKuuJEU6DvEytJyTMXCE7OLG/MpDBQcQclJKHgiyQm5gIOQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.6.8",
"core-js": "^2.5.7"
@@ -1750,7 +1744,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.44",
"caniuse-lite": "^1.0.30001806",
@@ -2961,6 +2954,35 @@
"fsevents": "^2.3.2"
}
},
"node_modules/jest-junit": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz",
"integrity": "sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"mkdirp": "^1.0.4",
"strip-ansi": "^6.0.1",
"uuid": "^8.3.2",
"xml": "^1.0.1"
},
"engines": {
"node": ">=10.12.0"
}
},
"node_modules/jest-junit/node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true,
"license": "MIT",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/jest-leak-detector": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz",
@@ -4421,6 +4443,17 @@
"pako": "^1.0.5"
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
"dev": true,
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/v8-to-istanbul": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
@@ -4524,6 +4557,13 @@
"xtend": "^4.0.0"
}
},
"node_modules/xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
"dev": true,
"license": "MIT"
},
"node_modules/xml-parse-from-string": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz",
+2 -1
View File
@@ -4,7 +4,7 @@
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "jest --config jest.config.js",
"test:e2e": "jest --config jest.e2e.config.js",
"test:e2e:member": "jest --config jest.e2e.config.js --testPathPattern=member"
},
@@ -19,6 +19,7 @@
},
"devDependencies": {
"jest": "^29.0.0",
"jest-junit": "^16.0.0",
"miniprogram-automator": "^0.12.1"
}
}
+1 -1
View File
@@ -57,4 +57,4 @@ function generateSignatureHeaders(method, url, body) {
}
}
module.exports = { generateSignatureHeaders }
module.exports = { generateSignatureHeaders, parseUrl, generateSignature, generateNonce }
+31
View File
@@ -0,0 +1,31 @@
const { generateSignatureHeaders, parseUrl } = require('./signature');
describe('signature utils', () => {
test('parseUrl 应正确分离 path 和 query', () => {
const result = parseUrl('https://api.example.com/api/users?id=1');
expect(result.path).toBe('/api/users');
expect(result.query).toBe('id=1');
});
test('parseUrl 无 query 时返回空 query', () => {
const result = parseUrl('/api/users');
expect(result.path).toBe('/api/users');
expect(result.query).toBe('');
});
test('generateSignatureHeaders 应生成包含必要字段的请求头', () => {
const headers = generateSignatureHeaders('GET', 'https://api.example.com/api/users', null);
expect(headers).toHaveProperty('X-Signature');
expect(headers).toHaveProperty('X-Timestamp');
expect(headers).toHaveProperty('X-Nonce');
expect(headers['X-Signature']).toBeTruthy();
expect(Number(headers['X-Timestamp'])).toBeGreaterThan(0);
});
test('相同请求参数应生成不同 nonce 的签名', () => {
const headers1 = generateSignatureHeaders('POST', 'https://api.example.com/api/users', { name: 'test' });
const headers2 = generateSignatureHeaders('POST', 'https://api.example.com/api/users', { name: 'test' });
expect(headers1['X-Nonce']).not.toBe(headers2['X-Nonce']);
expect(headers1['X-Signature']).not.toBe(headers2['X-Signature']);
});
});