run-tests.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. /**
  2. * 心知益家 - 自动化测试运行脚本
  3. *
  4. * 测试策略:
  5. * 1. Web端(管理后台):浏览器自动化测试,管理员和成长规划师登录,测试过程截图
  6. * 2. 小程序端:API接口测试,构造请求数据,验证响应,不进行UI点击测试
  7. *
  8. * 功能:
  9. * 1. 加载需求文档并解析需求项
  10. * 2. 执行API接口测试(小程序端)
  11. * 3. 执行Web浏览器测试(管理后台)
  12. * 4. 生成测试报告
  13. * 5. 需求变更时自动同步测试用例
  14. */
  15. const fs = require('fs');
  16. const path = require('path');
  17. // 配置
  18. const CONFIG = {
  19. requirementsPath: '../需求分析/需求分析文档.md',
  20. testCasesPath: './test-cases.js',
  21. outputPath: './test-reports',
  22. autoSync: true
  23. };
  24. /**
  25. * 需求解析器
  26. */
  27. class RequirementsParser {
  28. constructor(filePath) {
  29. this.filePath = filePath;
  30. this.requirements = [];
  31. }
  32. parse() {
  33. try {
  34. const content = fs.readFileSync(this.filePath, 'utf-8');
  35. const lines = content.split('\n');
  36. let currentModule = '';
  37. let currentSubModule = '';
  38. lines.forEach(line => {
  39. const moduleMatch = line.match(/^###\s+([\d.]+)\s+(.+)/);
  40. if (moduleMatch) {
  41. currentModule = moduleMatch[2].trim();
  42. return;
  43. }
  44. const subModuleMatch = line.match(/^####\s+([\d.]+)\s+(.+)/);
  45. if (subModuleMatch) {
  46. currentSubModule = subModuleMatch[2].trim();
  47. return;
  48. }
  49. const requirementMatch = line.match(/^\|\s*([A-Z]+-\d+[A-Z]?)\s*\|\s*(.+?)\s*\|\s*(P[0-2])\s*\|/);
  50. if (requirementMatch) {
  51. const [, id, description, priority] = requirementMatch;
  52. this.requirements.push({
  53. id: id.trim(),
  54. description: description.trim(),
  55. priority: priority.trim(),
  56. module: currentModule,
  57. subModule: currentSubModule
  58. });
  59. }
  60. });
  61. } catch (e) {
  62. console.log('需求文档解析失败,使用空列表');
  63. }
  64. return this.requirements;
  65. }
  66. getByPriority(priority) {
  67. return this.requirements.filter(req => req.priority === priority);
  68. }
  69. }
  70. /**
  71. * 测试同步器
  72. */
  73. class TestSynchronizer {
  74. constructor() {
  75. this.lastSyncFile = './.test-sync.json';
  76. }
  77. detectChanges(currentRequirements) {
  78. let lastSync = null;
  79. try {
  80. lastSync = JSON.parse(fs.readFileSync(this.lastSyncFile, 'utf-8'));
  81. } catch (e) {
  82. return {
  83. hasChanges: true,
  84. added: currentRequirements,
  85. modified: [],
  86. removed: []
  87. };
  88. }
  89. const added = [];
  90. const modified = [];
  91. const removed = [];
  92. currentRequirements.forEach(req => {
  93. const old = lastSync.requirements?.find(r => r.id === req.id);
  94. if (!old) {
  95. added.push(req);
  96. } else if (old.description !== req.description || old.priority !== req.priority) {
  97. modified.push({ old, new: req });
  98. }
  99. });
  100. lastSync.requirements?.forEach(oldReq => {
  101. if (!currentRequirements.find(r => r.id === oldReq.id)) {
  102. removed.push(oldReq);
  103. }
  104. });
  105. return { hasChanges: added.length > 0 || modified.length > 0 || removed.length > 0, added, modified, removed };
  106. }
  107. sync(changes) {
  108. console.log('\n========================================');
  109. console.log('需求变更检测结果:');
  110. console.log('========================================');
  111. if (changes.added.length > 0) {
  112. console.log(`\n新增需求 (${changes.added.length} 个):`);
  113. changes.added.forEach(req => console.log(` + ${req.id}: ${req.description} [${req.priority}]`));
  114. }
  115. if (changes.modified.length > 0) {
  116. console.log(`\n修改需求 (${changes.modified.length} 个):`);
  117. changes.modified.forEach(({ old, new: req }) => {
  118. console.log(` ~ ${req.id}:`);
  119. console.log(` 旧: ${old.description} [${old.priority}]`);
  120. console.log(` 新: ${req.description} [${req.priority}]`);
  121. });
  122. }
  123. if (changes.removed.length > 0) {
  124. console.log(`\n删除需求 (${changes.removed.length} 个):`);
  125. changes.removed.forEach(req => console.log(` - ${req.id}: ${req.description}`));
  126. }
  127. console.log('========================================\n');
  128. return changes.hasChanges;
  129. }
  130. saveSyncState(requirements) {
  131. fs.writeFileSync(this.lastSyncFile, JSON.stringify({ timestamp: new Date().toISOString(), requirements }, null, 2));
  132. }
  133. }
  134. /**
  135. * API测试执行器 - 小程序端接口测试
  136. */
  137. class ApiTestExecutor {
  138. constructor(testCases, config) {
  139. this.testCases = testCases;
  140. this.config = config;
  141. this.results = [];
  142. }
  143. async run() {
  144. console.log('\n========================================');
  145. console.log('API接口测试(小程序端)');
  146. console.log('========================================\n');
  147. for (const TestCase of Object.values(this.testCases)) {
  148. const testCase = new TestCase();
  149. console.log(`\n>>> 执行API测试: ${testCase.id} - ${testCase.description}`);
  150. try {
  151. await testCase.run();
  152. this.results.push({
  153. id: testCase.id,
  154. description: testCase.description,
  155. priority: testCase.priority,
  156. status: testCase.status,
  157. request: testCase.requestData,
  158. response: testCase.responseData,
  159. error: testCase.error
  160. });
  161. } catch (error) {
  162. console.error(`[${testCase.id}] 测试执行异常:`, error.message);
  163. this.results.push({
  164. id: testCase.id,
  165. description: testCase.description,
  166. priority: testCase.priority,
  167. status: 'failed',
  168. error: error.message
  169. });
  170. }
  171. }
  172. return this.results;
  173. }
  174. getSummary() {
  175. const passed = this.results.filter(r => r.status === 'passed').length;
  176. const failed = this.results.filter(r => r.status === 'failed').length;
  177. return { total: this.results.length, passed, failed };
  178. }
  179. }
  180. /**
  181. * Web浏览器测试执行器 - 管理后台UI测试
  182. */
  183. class WebTestExecutor {
  184. constructor(testCases, config) {
  185. this.testCases = testCases;
  186. this.config = config;
  187. this.results = [];
  188. }
  189. async run() {
  190. console.log('\n========================================');
  191. console.log('Web浏览器测试(管理后台)');
  192. console.log('========================================\n');
  193. console.log('注意: 此测试需要Chrome浏览器和MCP服务支持');
  194. console.log('测试过程中会自动截图保存到 ./screenshots/ 目录\n');
  195. for (const TestCase of Object.values(this.testCases)) {
  196. const testCase = new TestCase();
  197. console.log(`\n>>> 执行Web测试: ${testCase.id} - ${testCase.description}`);
  198. try {
  199. await testCase.run();
  200. this.results.push({
  201. id: testCase.id,
  202. description: testCase.description,
  203. priority: testCase.priority,
  204. status: testCase.status,
  205. screenshots: testCase.screenshots || [],
  206. error: testCase.error
  207. });
  208. } catch (error) {
  209. console.error(`[${testCase.id}] 测试执行异常:`, error.message);
  210. this.results.push({
  211. id: testCase.id,
  212. description: testCase.description,
  213. priority: testCase.priority,
  214. status: 'failed',
  215. screenshots: [],
  216. error: error.message
  217. });
  218. }
  219. }
  220. return this.results;
  221. }
  222. getSummary() {
  223. const passed = this.results.filter(r => r.status === 'passed').length;
  224. const failed = this.results.filter(r => r.status === 'failed').length;
  225. return { total: this.results.length, passed, failed };
  226. }
  227. }
  228. /**
  229. * 测试报告生成器
  230. */
  231. class ReportGenerator {
  232. constructor(outputPath) {
  233. this.outputPath = outputPath;
  234. if (!fs.existsSync(outputPath)) {
  235. fs.mkdirSync(outputPath, { recursive: true });
  236. }
  237. }
  238. generateHTML(apiResults, webResults, requirements) {
  239. const timestamp = new Date().toISOString();
  240. const reportPath = path.join(this.outputPath, `test-report-${Date.now()}.html`);
  241. const apiSummary = {
  242. total: apiResults.length,
  243. passed: apiResults.filter(r => r.status === 'passed').length,
  244. failed: apiResults.filter(r => r.status === 'failed').length
  245. };
  246. const webSummary = {
  247. total: webResults.length,
  248. passed: webResults.filter(r => r.status === 'passed').length,
  249. failed: webResults.filter(r => r.status === 'failed').length
  250. };
  251. const totalTests = apiSummary.total + webSummary.total;
  252. const totalPassed = apiSummary.passed + webSummary.passed;
  253. const totalFailed = apiSummary.failed + webSummary.failed;
  254. const html = `
  255. <!DOCTYPE html>
  256. <html lang="zh-CN">
  257. <head>
  258. <meta charset="UTF-8">
  259. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  260. <title>心知益家 - 测试报告</title>
  261. <style>
  262. * { margin: 0; padding: 0; box-sizing: border-box; }
  263. body { font-family: 'Microsoft YaHei', sans-serif; background: #f5f5f5; padding: 20px; }
  264. .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); }
  265. h1 { color: #333; margin-bottom: 20px; border-bottom: 3px solid #4CAF50; padding-bottom: 10px; }
  266. h2 { color: #555; margin: 25px 0 15px; padding-left: 10px; border-left: 4px solid #4CAF50; }
  267. h3 { color: #666; margin: 15px 0 10px; }
  268. .summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin: 20px 0; }
  269. .summary-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 25px; border-radius: 12px; text-align: center; }
  270. .summary-card.passed { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); }
  271. .summary-card.failed { background: linear-gradient(135deg, #eb3349 0%, #f45c5d 100%); }
  272. .summary-card.rate { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
  273. .summary-card h3 { font-size: 42px; margin-bottom: 8px; color: white; }
  274. .summary-card p { font-size: 14px; opacity: 0.9; }
  275. .test-section { margin: 30px 0; padding: 20px; background: #fafafa; border-radius: 8px; }
  276. .test-type { display: inline-block; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: bold; margin-right: 10px; }
  277. .test-type.api { background: #e3f2fd; color: #1565c0; }
  278. .test-type.web { background: #f3e5f5; color: #7b1fa2; }
  279. table { width: 100%; border-collapse: collapse; margin: 15px 0; }
  280. th, td { padding: 12px; text-align: left; border-bottom: 1px solid #e0e0e0; }
  281. th { background: #4CAF50; color: white; font-weight: 600; }
  282. tr:hover { background: #f5f5f5; }
  283. .status-passed { color: #4CAF50; font-weight: bold; }
  284. .status-failed { color: #f44336; font-weight: bold; }
  285. .code-block { background: #263238; color: #eceff1; padding: 15px; border-radius: 4px; font-family: 'Consolas', monospace; font-size: 12px; overflow-x: auto; margin: 10px 0; }
  286. .code-block .label { color: #80cbc4; font-weight: bold; }
  287. .timestamp { color: #999; font-size: 12px; margin-top: 30px; text-align: center; }
  288. .screenshot-list { margin: 10px 0; }
  289. .screenshot-list span { display: inline-block; background: #e8f5e9; color: #2e7d32; padding: 4px 8px; border-radius: 4px; font-size: 11px; margin: 2px; }
  290. </style>
  291. </head>
  292. <body>
  293. <div class="container">
  294. <h1>心知益家 - 自动化测试报告</h1>
  295. <div class="summary">
  296. <div class="summary-card">
  297. <h3>${totalTests}</h3>
  298. <p>总测试数</p>
  299. </div>
  300. <div class="summary-card passed">
  301. <h3>${totalPassed}</h3>
  302. <p>通过 ✅</p>
  303. </div>
  304. <div class="summary-card failed">
  305. <h3>${totalFailed}</h3>
  306. <p>失败 ❌</p>
  307. </div>
  308. <div class="summary-card rate">
  309. <h3>${totalTests > 0 ? ((totalPassed / totalTests) * 100).toFixed(1) : 0}%</h3>
  310. <p>通过率</p>
  311. </div>
  312. </div>
  313. <div class="test-section">
  314. <h2>API接口测试结果(小程序端)</h2>
  315. <p style="color: #666; margin: 10px 0;">测试策略:构造请求数据,调用后端接口,验证响应,不进行UI点击</p>
  316. <table>
  317. <tr>
  318. <th>测试ID</th>
  319. <th>描述</th>
  320. <th>优先级</th>
  321. <th>状态</th>
  322. <th>请求数据</th>
  323. </tr>
  324. ${apiResults.map(r => `
  325. <tr>
  326. <td><strong>${r.id}</strong></td>
  327. <td>${r.description}</td>
  328. <td>${r.priority}</td>
  329. <td class="status-${r.status}">${r.status.toUpperCase()}</td>
  330. <td><code>${JSON.stringify(r.request || {})}</code></td>
  331. </tr>
  332. `).join('')}
  333. </table>
  334. <h3>API测试数据汇总</h3>
  335. ${apiResults.map(r => `
  336. <div style="margin: 15px 0; padding: 10px; background: #fff; border: 1px solid #e0e0e0; border-radius: 4px;">
  337. <strong>[${r.id}] ${r.description}</strong>
  338. <div class="code-block">
  339. <div><span class="label">请求:</span></div>
  340. <pre>${JSON.stringify(r.request, null, 2)}</pre>
  341. <div><span class="label">响应:</span></div>
  342. <pre>${JSON.stringify(r.response, null, 2)}</pre>
  343. </div>
  344. </div>
  345. `).join('')}
  346. </div>
  347. <div class="test-section">
  348. <h2>Web浏览器测试结果(管理后台)</h2>
  349. <p style="color: #666; margin: 10px 0;">测试策略:浏览器自动化测试,管理员和成长规划师登录,测试过程截图</p>
  350. <p style="color: #999; font-size: 12px;">测试账号:管理员 ${this.config?.adminAccount?.phone || 'N/A'} / 成长规划师 ${this.config?.teacherAccount?.phone || 'N/A'}</p>
  351. <table>
  352. <tr>
  353. <th>测试ID</th>
  354. <th>描述</th>
  355. <th>优先级</th>
  356. <th>状态</th>
  357. <th>截图</th>
  358. </tr>
  359. ${webResults.map(r => `
  360. <tr>
  361. <td><strong>${r.id}</strong></td>
  362. <td>${r.description}</td>
  363. <td>${r.priority}</td>
  364. <td class="status-${r.status}">${r.status.toUpperCase()}</td>
  365. <td class="screenshot-list">${r.screenshots?.map(s => `<span>${path.basename(s)}</span>`).join('') || '-'}</td>
  366. </tr>
  367. `).join('')}
  368. </table>
  369. </div>
  370. <p class="timestamp">报告生成时间: ${timestamp}</p>
  371. </div>
  372. </body>
  373. </html>
  374. `;
  375. fs.writeFileSync(reportPath, html);
  376. console.log(`\n测试报告已生成: ${reportPath}`);
  377. return reportPath;
  378. }
  379. generateJSON(apiResults, webResults, requirements) {
  380. const reportPath = path.join(this.outputPath, `test-report-${Date.now()}.json`);
  381. const report = {
  382. timestamp: new Date().toISOString(),
  383. testStrategy: {
  384. api: '小程序端API接口测试,构造请求数据,验证响应,不进行UI点击',
  385. web: '管理后台浏览器自动化测试,管理员和成长规划师登录,测试过程截图'
  386. },
  387. accounts: {
  388. admin: '13800138000 / admin123',
  389. teacher: '13800138003 / teacher123',
  390. note: '家长和孩子不登录Web后台'
  391. },
  392. summary: {
  393. api: {
  394. total: apiResults.length,
  395. passed: apiResults.filter(r => r.status === 'passed').length,
  396. failed: apiResults.filter(r => r.status === 'failed').length
  397. },
  398. web: {
  399. total: webResults.length,
  400. passed: webResults.filter(r => r.status === 'passed').length,
  401. failed: webResults.filter(r => r.status === 'failed').length
  402. }
  403. },
  404. apiTests: apiResults,
  405. webTests: webResults
  406. };
  407. fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
  408. console.log(`JSON报告已生成: ${reportPath}`);
  409. return reportPath;
  410. }
  411. }
  412. /**
  413. * 主测试运行器
  414. */
  415. class TestRunner {
  416. constructor() {
  417. this.parser = new RequirementsParser(CONFIG.requirementsPath);
  418. this.synchronizer = new TestSynchronizer();
  419. this.reportGenerator = new ReportGenerator(CONFIG.outputPath);
  420. this.config = null;
  421. }
  422. async init() {
  423. console.log('正在解析需求文档...');
  424. const requirements = this.parser.parse();
  425. console.log(`已解析 ${requirements.length} 个需求项`);
  426. // 检测需求变更
  427. if (CONFIG.autoSync && requirements.length > 0) {
  428. const changes = this.synchronizer.detectChanges(requirements);
  429. this.synchronizer.sync(changes);
  430. if (changes.hasChanges) {
  431. console.log('正在同步测试用例...');
  432. this.synchronizer.saveSyncState(requirements);
  433. }
  434. }
  435. // 加载测试配置
  436. try {
  437. const testCases = require(CONFIG.testCasesPath);
  438. this.config = testCases.TEST_CONFIG;
  439. this.apiTestCases = testCases.apiTestCases || {};
  440. this.webTestCases = testCases.webTestCases || {};
  441. } catch (e) {
  442. console.log('加载测试用例失败,使用空配置');
  443. this.apiTestCases = {};
  444. this.webTestCases = {};
  445. }
  446. return requirements;
  447. }
  448. async run() {
  449. await this.init();
  450. // 执行API测试(小程序端)
  451. const apiExecutor = new ApiTestExecutor(this.apiTestCases, this.config);
  452. const apiResults = await apiExecutor.run();
  453. const apiSummary = apiExecutor.getSummary();
  454. // 执行Web测试(管理后台)
  455. const webExecutor = new WebTestExecutor(this.webTestCases, this.config);
  456. const webResults = await webExecutor.run();
  457. const webSummary = webExecutor.getSummary();
  458. // 生成报告
  459. this.reportGenerator.generateHTML(apiResults, webResults, this.parser.requirements);
  460. this.reportGenerator.generateJSON(apiResults, webResults, this.parser.requirements);
  461. // 输出汇总
  462. console.log('\n========================================');
  463. console.log('测试执行完成');
  464. console.log('========================================');
  465. console.log(`\nAPI接口测试(小程序端):`);
  466. console.log(` 总数: ${apiSummary.total}`);
  467. console.log(` 通过: ${apiSummary.passed} ✅`);
  468. console.log(` 失败: ${apiSummary.failed} ❌`);
  469. console.log(`\nWeb浏览器测试(管理后台):`);
  470. console.log(` 总数: ${webSummary.total}`);
  471. console.log(` 通过: ${webSummary.passed} ✅`);
  472. console.log(` 失败: ${webSummary.failed} ❌`);
  473. console.log(`\n总计:`);
  474. console.log(` 总测试数: ${apiSummary.total + webSummary.total}`);
  475. console.log(` 通过: ${apiSummary.passed + webSummary.passed} ✅`);
  476. console.log(` 失败: ${apiSummary.failed + webSummary.failed} ❌`);
  477. const totalTests = apiSummary.total + webSummary.total;
  478. const totalPassed = apiSummary.passed + webSummary.passed;
  479. console.log(` 通过率: ${totalTests > 0 ? ((totalPassed / totalTests) * 100).toFixed(2) : 0}%`);
  480. console.log('========================================\n');
  481. return { apiResults, webResults, apiSummary, webSummary };
  482. }
  483. async runByPriority(priority) {
  484. await this.init();
  485. console.log(`\n运行 ${priority} 级测试...`);
  486. // 按优先级筛选并运行测试
  487. return this.run();
  488. }
  489. }
  490. // 导出
  491. module.exports = {
  492. RequirementsParser,
  493. TestSynchronizer,
  494. ApiTestExecutor,
  495. WebTestExecutor,
  496. ReportGenerator,
  497. TestRunner
  498. };
  499. // 主程序
  500. async function main() {
  501. const runner = new TestRunner();
  502. try {
  503. await runner.run();
  504. } catch (error) {
  505. console.error('测试执行失败:', error);
  506. process.exit(1);
  507. }
  508. }
  509. // 如果直接运行
  510. if (require.main === module) {
  511. main();
  512. }