| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- # -*- 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)
|