| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582 |
- /**
- * 心知益家 - 自动化测试运行脚本
- *
- * 测试策略:
- * 1. Web端(管理后台):浏览器自动化测试,管理员和成长规划师登录,测试过程截图
- * 2. 小程序端:API接口测试,构造请求数据,验证响应,不进行UI点击测试
- *
- * 功能:
- * 1. 加载需求文档并解析需求项
- * 2. 执行API接口测试(小程序端)
- * 3. 执行Web浏览器测试(管理后台)
- * 4. 生成测试报告
- * 5. 需求变更时自动同步测试用例
- */
- const fs = require('fs');
- const path = require('path');
- // 配置
- const CONFIG = {
- requirementsPath: '../需求分析/需求分析文档.md',
- testCasesPath: './test-cases.js',
- outputPath: './test-reports',
- autoSync: true
- };
- /**
- * 需求解析器
- */
- class RequirementsParser {
- constructor(filePath) {
- this.filePath = filePath;
- this.requirements = [];
- }
- parse() {
- try {
- const content = fs.readFileSync(this.filePath, 'utf-8');
- const lines = content.split('\n');
- let currentModule = '';
- let currentSubModule = '';
- lines.forEach(line => {
- const moduleMatch = line.match(/^###\s+([\d.]+)\s+(.+)/);
- if (moduleMatch) {
- currentModule = moduleMatch[2].trim();
- return;
- }
- const subModuleMatch = line.match(/^####\s+([\d.]+)\s+(.+)/);
- if (subModuleMatch) {
- currentSubModule = subModuleMatch[2].trim();
- return;
- }
- const requirementMatch = line.match(/^\|\s*([A-Z]+-\d+[A-Z]?)\s*\|\s*(.+?)\s*\|\s*(P[0-2])\s*\|/);
- if (requirementMatch) {
- const [, id, description, priority] = requirementMatch;
- this.requirements.push({
- id: id.trim(),
- description: description.trim(),
- priority: priority.trim(),
- module: currentModule,
- subModule: currentSubModule
- });
- }
- });
- } catch (e) {
- console.log('需求文档解析失败,使用空列表');
- }
- return this.requirements;
- }
- getByPriority(priority) {
- return this.requirements.filter(req => req.priority === priority);
- }
- }
- /**
- * 测试同步器
- */
- class TestSynchronizer {
- constructor() {
- this.lastSyncFile = './.test-sync.json';
- }
- detectChanges(currentRequirements) {
- let lastSync = null;
- try {
- lastSync = JSON.parse(fs.readFileSync(this.lastSyncFile, 'utf-8'));
- } catch (e) {
- return {
- hasChanges: true,
- added: currentRequirements,
- modified: [],
- removed: []
- };
- }
- const added = [];
- const modified = [];
- const removed = [];
- currentRequirements.forEach(req => {
- const old = lastSync.requirements?.find(r => r.id === req.id);
- if (!old) {
- added.push(req);
- } else if (old.description !== req.description || old.priority !== req.priority) {
- modified.push({ old, new: req });
- }
- });
- lastSync.requirements?.forEach(oldReq => {
- if (!currentRequirements.find(r => r.id === oldReq.id)) {
- removed.push(oldReq);
- }
- });
- return { hasChanges: added.length > 0 || modified.length > 0 || removed.length > 0, added, modified, removed };
- }
- sync(changes) {
- console.log('\n========================================');
- console.log('需求变更检测结果:');
- console.log('========================================');
- if (changes.added.length > 0) {
- console.log(`\n新增需求 (${changes.added.length} 个):`);
- changes.added.forEach(req => console.log(` + ${req.id}: ${req.description} [${req.priority}]`));
- }
- if (changes.modified.length > 0) {
- console.log(`\n修改需求 (${changes.modified.length} 个):`);
- changes.modified.forEach(({ old, new: req }) => {
- console.log(` ~ ${req.id}:`);
- console.log(` 旧: ${old.description} [${old.priority}]`);
- console.log(` 新: ${req.description} [${req.priority}]`);
- });
- }
- if (changes.removed.length > 0) {
- console.log(`\n删除需求 (${changes.removed.length} 个):`);
- changes.removed.forEach(req => console.log(` - ${req.id}: ${req.description}`));
- }
- console.log('========================================\n');
- return changes.hasChanges;
- }
- saveSyncState(requirements) {
- fs.writeFileSync(this.lastSyncFile, JSON.stringify({ timestamp: new Date().toISOString(), requirements }, null, 2));
- }
- }
- /**
- * API测试执行器 - 小程序端接口测试
- */
- class ApiTestExecutor {
- constructor(testCases, config) {
- this.testCases = testCases;
- this.config = config;
- this.results = [];
- }
- async run() {
- console.log('\n========================================');
- console.log('API接口测试(小程序端)');
- console.log('========================================\n');
- for (const TestCase of Object.values(this.testCases)) {
- const testCase = new TestCase();
- console.log(`\n>>> 执行API测试: ${testCase.id} - ${testCase.description}`);
-
- try {
- await testCase.run();
- this.results.push({
- id: testCase.id,
- description: testCase.description,
- priority: testCase.priority,
- status: testCase.status,
- request: testCase.requestData,
- response: testCase.responseData,
- error: testCase.error
- });
- } catch (error) {
- console.error(`[${testCase.id}] 测试执行异常:`, error.message);
- this.results.push({
- id: testCase.id,
- description: testCase.description,
- priority: testCase.priority,
- status: 'failed',
- error: error.message
- });
- }
- }
- return this.results;
- }
- getSummary() {
- const passed = this.results.filter(r => r.status === 'passed').length;
- const failed = this.results.filter(r => r.status === 'failed').length;
- return { total: this.results.length, passed, failed };
- }
- }
- /**
- * Web浏览器测试执行器 - 管理后台UI测试
- */
- class WebTestExecutor {
- constructor(testCases, config) {
- this.testCases = testCases;
- this.config = config;
- this.results = [];
- }
- async run() {
- console.log('\n========================================');
- console.log('Web浏览器测试(管理后台)');
- console.log('========================================\n');
- console.log('注意: 此测试需要Chrome浏览器和MCP服务支持');
- console.log('测试过程中会自动截图保存到 ./screenshots/ 目录\n');
- for (const TestCase of Object.values(this.testCases)) {
- const testCase = new TestCase();
- console.log(`\n>>> 执行Web测试: ${testCase.id} - ${testCase.description}`);
-
- try {
- await testCase.run();
- this.results.push({
- id: testCase.id,
- description: testCase.description,
- priority: testCase.priority,
- status: testCase.status,
- screenshots: testCase.screenshots || [],
- error: testCase.error
- });
- } catch (error) {
- console.error(`[${testCase.id}] 测试执行异常:`, error.message);
- this.results.push({
- id: testCase.id,
- description: testCase.description,
- priority: testCase.priority,
- status: 'failed',
- screenshots: [],
- error: error.message
- });
- }
- }
- return this.results;
- }
- getSummary() {
- const passed = this.results.filter(r => r.status === 'passed').length;
- const failed = this.results.filter(r => r.status === 'failed').length;
- return { total: this.results.length, passed, failed };
- }
- }
- /**
- * 测试报告生成器
- */
- class ReportGenerator {
- constructor(outputPath) {
- this.outputPath = outputPath;
- if (!fs.existsSync(outputPath)) {
- fs.mkdirSync(outputPath, { recursive: true });
- }
- }
- generateHTML(apiResults, webResults, requirements) {
- const timestamp = new Date().toISOString();
- const reportPath = path.join(this.outputPath, `test-report-${Date.now()}.html`);
- const apiSummary = {
- total: apiResults.length,
- passed: apiResults.filter(r => r.status === 'passed').length,
- failed: apiResults.filter(r => r.status === 'failed').length
- };
- const webSummary = {
- total: webResults.length,
- passed: webResults.filter(r => r.status === 'passed').length,
- failed: webResults.filter(r => r.status === 'failed').length
- };
- const totalTests = apiSummary.total + webSummary.total;
- const totalPassed = apiSummary.passed + webSummary.passed;
- const totalFailed = apiSummary.failed + webSummary.failed;
- const html = `
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>心知益家 - 测试报告</title>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body { font-family: 'Microsoft YaHei', sans-serif; background: #f5f5f5; padding: 20px; }
- .container { max-width: 1400px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
- h1 { color: #333; margin-bottom: 20px; border-bottom: 3px solid #4CAF50; padding-bottom: 10px; }
- h2 { color: #555; margin: 25px 0 15px; padding-left: 10px; border-left: 4px solid #4CAF50; }
- h3 { color: #666; margin: 15px 0 10px; }
-
- .summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin: 20px 0; }
- .summary-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 25px; border-radius: 12px; text-align: center; }
- .summary-card.passed { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); }
- .summary-card.failed { background: linear-gradient(135deg, #eb3349 0%, #f45c5d 100%); }
- .summary-card.rate { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
- .summary-card h3 { font-size: 42px; margin-bottom: 8px; color: white; }
- .summary-card p { font-size: 14px; opacity: 0.9; }
- .test-section { margin: 30px 0; padding: 20px; background: #fafafa; border-radius: 8px; }
- .test-type { display: inline-block; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: bold; margin-right: 10px; }
- .test-type.api { background: #e3f2fd; color: #1565c0; }
- .test-type.web { background: #f3e5f5; color: #7b1fa2; }
- table { width: 100%; border-collapse: collapse; margin: 15px 0; }
- th, td { padding: 12px; text-align: left; border-bottom: 1px solid #e0e0e0; }
- th { background: #4CAF50; color: white; font-weight: 600; }
- tr:hover { background: #f5f5f5; }
- .status-passed { color: #4CAF50; font-weight: bold; }
- .status-failed { color: #f44336; font-weight: bold; }
-
- .code-block { background: #263238; color: #eceff1; padding: 15px; border-radius: 4px; font-family: 'Consolas', monospace; font-size: 12px; overflow-x: auto; margin: 10px 0; }
- .code-block .label { color: #80cbc4; font-weight: bold; }
- .timestamp { color: #999; font-size: 12px; margin-top: 30px; text-align: center; }
-
- .screenshot-list { margin: 10px 0; }
- .screenshot-list span { display: inline-block; background: #e8f5e9; color: #2e7d32; padding: 4px 8px; border-radius: 4px; font-size: 11px; margin: 2px; }
- </style>
- </head>
- <body>
- <div class="container">
- <h1>心知益家 - 自动化测试报告</h1>
- <div class="summary">
- <div class="summary-card">
- <h3>${totalTests}</h3>
- <p>总测试数</p>
- </div>
- <div class="summary-card passed">
- <h3>${totalPassed}</h3>
- <p>通过 ✅</p>
- </div>
- <div class="summary-card failed">
- <h3>${totalFailed}</h3>
- <p>失败 ❌</p>
- </div>
- <div class="summary-card rate">
- <h3>${totalTests > 0 ? ((totalPassed / totalTests) * 100).toFixed(1) : 0}%</h3>
- <p>通过率</p>
- </div>
- </div>
- <div class="test-section">
- <h2>API接口测试结果(小程序端)</h2>
- <p style="color: #666; margin: 10px 0;">测试策略:构造请求数据,调用后端接口,验证响应,不进行UI点击</p>
- <table>
- <tr>
- <th>测试ID</th>
- <th>描述</th>
- <th>优先级</th>
- <th>状态</th>
- <th>请求数据</th>
- </tr>
- ${apiResults.map(r => `
- <tr>
- <td><strong>${r.id}</strong></td>
- <td>${r.description}</td>
- <td>${r.priority}</td>
- <td class="status-${r.status}">${r.status.toUpperCase()}</td>
- <td><code>${JSON.stringify(r.request || {})}</code></td>
- </tr>
- `).join('')}
- </table>
- <h3>API测试数据汇总</h3>
- ${apiResults.map(r => `
- <div style="margin: 15px 0; padding: 10px; background: #fff; border: 1px solid #e0e0e0; border-radius: 4px;">
- <strong>[${r.id}] ${r.description}</strong>
- <div class="code-block">
- <div><span class="label">请求:</span></div>
- <pre>${JSON.stringify(r.request, null, 2)}</pre>
- <div><span class="label">响应:</span></div>
- <pre>${JSON.stringify(r.response, null, 2)}</pre>
- </div>
- </div>
- `).join('')}
- </div>
- <div class="test-section">
- <h2>Web浏览器测试结果(管理后台)</h2>
- <p style="color: #666; margin: 10px 0;">测试策略:浏览器自动化测试,管理员和成长规划师登录,测试过程截图</p>
- <p style="color: #999; font-size: 12px;">测试账号:管理员 ${this.config?.adminAccount?.phone || 'N/A'} / 成长规划师 ${this.config?.teacherAccount?.phone || 'N/A'}</p>
- <table>
- <tr>
- <th>测试ID</th>
- <th>描述</th>
- <th>优先级</th>
- <th>状态</th>
- <th>截图</th>
- </tr>
- ${webResults.map(r => `
- <tr>
- <td><strong>${r.id}</strong></td>
- <td>${r.description}</td>
- <td>${r.priority}</td>
- <td class="status-${r.status}">${r.status.toUpperCase()}</td>
- <td class="screenshot-list">${r.screenshots?.map(s => `<span>${path.basename(s)}</span>`).join('') || '-'}</td>
- </tr>
- `).join('')}
- </table>
- </div>
- <p class="timestamp">报告生成时间: ${timestamp}</p>
- </div>
- </body>
- </html>
- `;
- fs.writeFileSync(reportPath, html);
- console.log(`\n测试报告已生成: ${reportPath}`);
- return reportPath;
- }
- generateJSON(apiResults, webResults, requirements) {
- const reportPath = path.join(this.outputPath, `test-report-${Date.now()}.json`);
- const report = {
- timestamp: new Date().toISOString(),
- testStrategy: {
- api: '小程序端API接口测试,构造请求数据,验证响应,不进行UI点击',
- web: '管理后台浏览器自动化测试,管理员和成长规划师登录,测试过程截图'
- },
- accounts: {
- admin: '13800138000 / admin123',
- teacher: '13800138003 / teacher123',
- note: '家长和孩子不登录Web后台'
- },
- summary: {
- api: {
- total: apiResults.length,
- passed: apiResults.filter(r => r.status === 'passed').length,
- failed: apiResults.filter(r => r.status === 'failed').length
- },
- web: {
- total: webResults.length,
- passed: webResults.filter(r => r.status === 'passed').length,
- failed: webResults.filter(r => r.status === 'failed').length
- }
- },
- apiTests: apiResults,
- webTests: webResults
- };
- fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
- console.log(`JSON报告已生成: ${reportPath}`);
- return reportPath;
- }
- }
- /**
- * 主测试运行器
- */
- class TestRunner {
- constructor() {
- this.parser = new RequirementsParser(CONFIG.requirementsPath);
- this.synchronizer = new TestSynchronizer();
- this.reportGenerator = new ReportGenerator(CONFIG.outputPath);
- this.config = null;
- }
- async init() {
- console.log('正在解析需求文档...');
- const requirements = this.parser.parse();
- console.log(`已解析 ${requirements.length} 个需求项`);
- // 检测需求变更
- if (CONFIG.autoSync && requirements.length > 0) {
- const changes = this.synchronizer.detectChanges(requirements);
- this.synchronizer.sync(changes);
- if (changes.hasChanges) {
- console.log('正在同步测试用例...');
- this.synchronizer.saveSyncState(requirements);
- }
- }
- // 加载测试配置
- try {
- const testCases = require(CONFIG.testCasesPath);
- this.config = testCases.TEST_CONFIG;
- this.apiTestCases = testCases.apiTestCases || {};
- this.webTestCases = testCases.webTestCases || {};
- } catch (e) {
- console.log('加载测试用例失败,使用空配置');
- this.apiTestCases = {};
- this.webTestCases = {};
- }
- return requirements;
- }
- async run() {
- await this.init();
- // 执行API测试(小程序端)
- const apiExecutor = new ApiTestExecutor(this.apiTestCases, this.config);
- const apiResults = await apiExecutor.run();
- const apiSummary = apiExecutor.getSummary();
- // 执行Web测试(管理后台)
- const webExecutor = new WebTestExecutor(this.webTestCases, this.config);
- const webResults = await webExecutor.run();
- const webSummary = webExecutor.getSummary();
- // 生成报告
- this.reportGenerator.generateHTML(apiResults, webResults, this.parser.requirements);
- this.reportGenerator.generateJSON(apiResults, webResults, this.parser.requirements);
- // 输出汇总
- console.log('\n========================================');
- console.log('测试执行完成');
- console.log('========================================');
- console.log(`\nAPI接口测试(小程序端):`);
- console.log(` 总数: ${apiSummary.total}`);
- console.log(` 通过: ${apiSummary.passed} ✅`);
- console.log(` 失败: ${apiSummary.failed} ❌`);
- console.log(`\nWeb浏览器测试(管理后台):`);
- console.log(` 总数: ${webSummary.total}`);
- console.log(` 通过: ${webSummary.passed} ✅`);
- console.log(` 失败: ${webSummary.failed} ❌`);
- console.log(`\n总计:`);
- console.log(` 总测试数: ${apiSummary.total + webSummary.total}`);
- console.log(` 通过: ${apiSummary.passed + webSummary.passed} ✅`);
- console.log(` 失败: ${apiSummary.failed + webSummary.failed} ❌`);
- const totalTests = apiSummary.total + webSummary.total;
- const totalPassed = apiSummary.passed + webSummary.passed;
- console.log(` 通过率: ${totalTests > 0 ? ((totalPassed / totalTests) * 100).toFixed(2) : 0}%`);
- console.log('========================================\n');
- return { apiResults, webResults, apiSummary, webSummary };
- }
- async runByPriority(priority) {
- await this.init();
- console.log(`\n运行 ${priority} 级测试...`);
- // 按优先级筛选并运行测试
- return this.run();
- }
- }
- // 导出
- module.exports = {
- RequirementsParser,
- TestSynchronizer,
- ApiTestExecutor,
- WebTestExecutor,
- ReportGenerator,
- TestRunner
- };
- // 主程序
- async function main() {
- const runner = new TestRunner();
- try {
- await runner.run();
- } catch (error) {
- console.error('测试执行失败:', error);
- process.exit(1);
- }
- }
- // 如果直接运行
- if (require.main === module) {
- main();
- }
|