# -*- coding: utf-8 -*- from pathlib import Path from docx import Document from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_TABLE_ALIGNMENT from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.oxml import OxmlElement from docx.oxml.ns import qn from docx.shared import Inches, Pt, RGBColor ROOT = Path(__file__).resolve().parent PLAN_DOCX = ROOT / "0235711-王刚-综合测试计划.docx" REPORT_DOCX = ROOT / "0235711-王刚-综合测试报告.docx" AUTO_ARTIFACTS = ROOT / "tests" / "auto_tests" / "artifacts" def format_run(run, font="宋体", size=10.5, bold=False, color=None): run.font.name = font run._element.rPr.rFonts.set(qn("w:eastAsia"), font) run.font.size = Pt(size) run.bold = bold if color: run.font.color.rgb = RGBColor(*color) def setup_doc(title, subtitle): doc = Document() section = doc.sections[0] section.top_margin = Inches(0.8) section.bottom_margin = Inches(0.8) section.left_margin = Inches(0.85) section.right_margin = Inches(0.85) doc.styles["Normal"].font.name = "宋体" doc.styles["Normal"]._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体") doc.styles["Normal"].font.size = Pt(10.5) p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER r = p.add_run(title) format_run(r, "微软雅黑", 20, True, (17, 24, 39)) p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER r = p.add_run(subtitle) format_run(r, "微软雅黑", 11, False, (82, 96, 113)) doc.add_paragraph("") return doc def h1(doc, text): p = doc.add_paragraph() p.paragraph_format.space_before = Pt(10) p.paragraph_format.space_after = Pt(6) r = p.add_run(text) format_run(r, "微软雅黑", 15, True, (23, 78, 166)) def h2(doc, text): p = doc.add_paragraph() p.paragraph_format.space_before = Pt(7) p.paragraph_format.space_after = Pt(4) r = p.add_run(text) format_run(r, "微软雅黑", 12, True, (17, 24, 39)) def body(doc, text): p = doc.add_paragraph(text) p.paragraph_format.first_line_indent = Pt(21) p.paragraph_format.line_spacing = 1.25 p.paragraph_format.space_after = Pt(4) for r in p.runs: format_run(r) def border(cell): tc_pr = cell._tc.get_or_add_tcPr() borders = tc_pr.first_child_found_in("w:tcBorders") if borders is None: borders = OxmlElement("w:tcBorders") tc_pr.append(borders) for edge in ("top", "left", "bottom", "right"): element = borders.find(qn(f"w:{edge}")) if element is None: element = OxmlElement(f"w:{edge}") borders.append(element) element.set(qn("w:val"), "single") element.set(qn("w:sz"), "6") element.set(qn("w:space"), "0") element.set(qn("w:color"), "D8DEE8") def shade(cell, fill): tc_pr = cell._tc.get_or_add_tcPr() shd = OxmlElement("w:shd") shd.set(qn("w:fill"), fill) tc_pr.append(shd) def cell_text(cell, text, bold=False, color=None): cell.text = "" p = cell.paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.CENTER if len(str(text)) <= 12 else WD_ALIGN_PARAGRAPH.LEFT r = p.add_run(str(text)) format_run(r, size=9.5, bold=bold, color=color) cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER border(cell) def table(doc, headers, rows): t = doc.add_table(rows=1, cols=len(headers)) t.alignment = WD_TABLE_ALIGNMENT.CENTER t.autofit = True for i, header in enumerate(headers): shade(t.rows[0].cells[i], "EAF1FF") cell_text(t.rows[0].cells[i], header, True, (23, 78, 166)) for row in rows: cells = t.add_row().cells for i, val in enumerate(row): cell_text(cells[i], val) doc.add_paragraph("") def caption(doc, text): p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER r = p.add_run(text) format_run(r, size=9, color=(82, 96, 113)) def picture(doc, filename, text): path = AUTO_ARTIFACTS / filename if path.exists(): p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.add_run().add_picture(str(path), width=Inches(6.1)) caption(doc, text) def build_plan(): doc = setup_doc("0235711-王刚-综合测试计划", "ServicePilot 智能客户工单与 SLA 履约管理平台") h1(doc, "一、测试范围") body(doc, "本项目测试对象包括前端 Angular 页面、后端 Node.js + TypeScript + Express 接口、Parse 数据权限、工单管理、SLA 管理、知识库、附件管理、满意度评价和数据看板。") table(doc, ["测试类型", "目录", "工具", "数量目标", "覆盖范围"], [ ["单元测试", "tests/unit_tests", "Jest", "26 条", "状态流转、权限、SLA、附件规则"], ["接口测试", "tests/api_tests", "Jest + Supertest", "5 条", "health、Parse 配置、404 契约"], ["自动化测试", "tests/auto_tests", "Playwright CLI", "7 条", "七个前端关键页面"], ["性能测试", "tests/performance_tests", "Locust", "2 个脚本", "后端接口、前端页面"], ]) h1(doc, "二、四类测试计划") h2(doc, "1. 单元测试计划") body(doc, "单元测试重点验证业务规则函数,不依赖浏览器和真实数据库。要求核心规则通过率达到 100%,整体通过率不低于 95%。") h2(doc, "2. 接口测试计划") body(doc, "接口测试重点验证 Express API 的响应状态、返回结构、错误处理和敏感字段保护。") h2(doc, "3. 自动化测试计划") body(doc, "自动化测试使用 Playwright CLI 打开构建后的前端页面,检查 HTTP 状态并保存页面截图。") h2(doc, "4. 性能测试计划") body(doc, "性能测试使用 Locust 对后端接口和前端页面进行并发访问,观察平均响应时间、P95、错误率和服务稳定性。") h1(doc, "三、验收标准") table(doc, ["指标", "标准"], [ ["单元测试通过率", ">= 95%,核心业务规则 100%"], ["接口测试通过率", ">= 95%"], ["自动化测试通过率", "100%"], ["性能测试错误率", "< 1%"], ["阻塞/严重缺陷", "0 个"], ]) doc.save(PLAN_DOCX) def build_report(): doc = setup_doc("0235711-王刚-综合测试报告", "按作业要求分为单元测试、接口测试、自动化测试、性能测试四部分") h1(doc, "一、测试执行汇总") table(doc, ["测试类型", "脚本目录", "执行数量", "通过数量", "结论"], [ ["单元测试", "tests/unit_tests", "26", "26", "通过"], ["接口测试", "tests/api_tests", "5", "5", "通过"], ["自动化测试", "tests/auto_tests", "7", "7", "通过"], ["性能测试", "tests/performance_tests", "2 个 Locust 脚本", "待执行", "脚本已补充"], ]) h1(doc, "二、单元测试结果") body(doc, "执行 tests/unit_tests/run_unit_tests.ps1,调用后端 Jest 测试 quality-rules.spec.ts。结果为 1 个测试套件通过,26 条用例全部通过,通过率 100%。") table(doc, ["模块", "测试点", "结果"], [ ["工单状态", "合法流转、非法流转、关闭状态处理", "通过"], ["权限隔离", "客户、客服、技术支持、管理员读取权限", "通过"], ["SLA", "截止时间、预警、超时判断", "通过"], ["附件", "类型限制、空文件、大小限制", "通过"], ]) h1(doc, "三、接口测试结果") body(doc, "执行 tests/api_tests/run_api_tests.ps1,调用 Jest + Supertest 测试 health.spec.ts 和 api-contract.spec.ts。结果为 2 个测试套件通过,5 条接口用例全部通过。") table(doc, ["接口", "测试内容", "结果"], [ ["GET /api/health", "服务健康检查", "通过"], ["GET /api/config/parse", "公开配置结构", "通过"], ["GET /api/config/parse", "敏感字段不暴露", "通过"], ["GET /api/unknown-resource", "404 契约", "通过"], ["POST /api/ghost", "404 消息包含方法和路径", "通过"], ]) h1(doc, "四、自动化测试结果") body(doc, "执行 tests/auto_tests/run_auto_tests.ps1,使用 Playwright CLI 对前端七个关键页面进行访问验证并保存截图。结果为 7 条自动化流程全部通过。") table(doc, ["编号", "页面", "结果"], [ ["AUTO-001", "/dashboard", "PASS"], ["AUTO-002", "/tickets", "PASS"], ["AUTO-003", "/sla", "PASS"], ["AUTO-004", "/knowledge", "PASS"], ["AUTO-005", "/files", "PASS"], ["AUTO-006", "/reviews", "PASS"], ["AUTO-007", "/test-plan", "PASS"], ]) picture(doc, "AUTO-001-dashboard.png", "图 1 自动化测试截图:工作台页面") picture(doc, "AUTO-002-tickets.png", "图 2 自动化测试截图:工单管理页面") picture(doc, "AUTO-003-sla.png", "图 3 自动化测试截图:SLA 管理页面") h1(doc, "五、性能测试脚本说明") body(doc, "性能测试目录已补充 Locust 脚本,包括 locustfile_backend.py 和 locustfile_frontend.py。由于当前机器未检测到 Locust 命令,本次未生成真实性能报告;安装 Locust 后可运行 run_backend_performance.ps1 和 run_frontend_performance.ps1 生成 HTML 报告。") table(doc, ["脚本", "测试目标", "默认并发配置"], [ ["locustfile_backend.py", "后端 health、Parse 配置、404 契约", "30 用户,启动速率 5,持续 3 分钟"], ["locustfile_frontend.py", "前端 dashboard、tickets、sla、knowledge、files、reviews、test-plan", "30 用户,启动速率 5,持续 3 分钟"], ]) h1(doc, "六、缺陷与结论") body(doc, "本次补充后,测试脚本目录已经符合课程作业结构要求:unit_tests、api_tests、auto_tests、performance_tests 均有对应脚本和说明。已执行的单元测试、接口测试、自动化测试通过率均为 100%;性能测试脚本已具备,待安装 Locust 后执行。") doc.save(REPORT_DOCX) if __name__ == "__main__": build_plan() build_report() print(PLAN_DOCX) print(REPORT_DOCX)