上篇结尾说了,从这篇开始进入实战板块——把前面三篇讲的Agent骨架、Prompt链、API接入全部串起来,造能落地的工具。
第一个就是测试用例生成器。
为什么选这个打头?因为它是我造的第一个真正"能用"的AI测试工具,也是踩坑最多的一个。前面三篇里提到的所有坑——Prompt太笼统、步骤衔接没容错、数据格式不对齐——基本都是在造这个工具的过程中踩的。
但那些是"基础搭建"的坑。真正把它从一个"能跑的脚本"变成"团队愿意用的工具",又踩了一轮完全不同的坑。
第一版:技术上跑通了,实际上没人用
我的第一版用例生成器,就是S4-03里写的那个三步Agent:拆维度→写用例→格式化。技术上完全跑通,输入PRD文本,30秒输出一批结构化用例。
我特别兴奋地拿给同事用。她试了一次,说:"挺好的,但我还是自己写比较快。"
我当时不服气。跟着她看了一遍她怎么写用例的:她拿到PRD后,先花5分钟通读,在关键段落画线标记;然后打开团队的Excel模板,按模块填用例;写的过程中她会翻看历史用例,看看类似功能上次怎么测的;写完还会跟开发确认几个边界场景。
我的工具帮她省了什么?省了"把PRD读一遍然后想测试点"这个环节。但这只是她写用例流程的第一步。后面的查历史用例、跟开发确认、填团队模板——这些我的工具都没覆盖。
你的工具如果只解决了用户工作流中的一个环节,而剩下的环节还得手动做,那用户切换工具的成本可能比直接手动做还高。
这是第一版最大的问题:我只想着"用AI生成用例",没想过"生成用例之后呢?"
第二版:补上"最后一公里"
第二版做了三件事,每件都是被现实打脸之后的反应。
第一件:导入团队Excel模板。
第一版输出的用例格式是我自己定义的JSON。同事拿过去还得手动转成团队模板格式。这个"最后一公里"把工具的效率优势全吃了。
我找同事要了团队的Excel模板,分析了一下结构——用例编号、模块、场景描述、前置条件、操作步骤、预期结果、优先级、备注。然后写了一个导出模块,直接按模板格式输出Excel。
这一步做完之后,同事的态度从"挺好的但我不太用"变成了"偶尔用一下"。进步了,但还不够。
第二件:加历史用例参考。
同事写用例时会翻历史用例,看看类似功能上次怎么测的。我的工具完全没这个能力——它只看当前PRD,不知道这个功能以前怎么测的。
我加了一步:在拆完维度之后,让Agent去测试平台搜同一模块的历史用例,把相关的作为参考喂给"写用例"那一步的Prompt。
这一步效果出乎意料地好。AI生成的用例质量明显提升——因为有了历史用例做参考,它不再只基于PRD字面内容"猜"测试点,而是结合了团队过去的测试经验。生成的用例风格也更贴近团队习惯,同事说"看着像是我们组的人写的"。
但这一步也带来了新问题:历史用例有时候太多,全喂进去会超token限制。我的处理方式是让Agent先筛选——只取和当前功能相关的、近三个月的、最多20条。筛选逻辑本身又是一个Prompt,加了十行代码,但效果天差地别。不筛选的时候,200条历史用例一股脑塞进去,大模型注意力被稀释,生成的用例反而比不加参考还差。
加参考数据不是越多越好,是"筛过的好"。这个教训后来我用在所有需要喂上下文的工具上。
第三件:加"覆盖率自查"。
这是被同事一句话逼出来的。有一次我拿生成的用例给她看,她扫了一眼说:"你这个登录模块的用例,没覆盖到'记住登录态'。"
我回去查,PRD里确实提了"记住登录态",但AI在拆维度的时候把它归到"登录功能"下面,写用例的时候又只写了"登录成功"和"登录失败","记住登录态"被遗漏了。
这不是S4-03里讲的"大模型遗忘排在前面的测试点"——那次是注意力衰减。这次是理解偏差:它把"记住登录态"理解成了登录功能的一个子项,而不是一个独立的测试场景。
我在格式化输出之前加了一个"覆盖率自查"步骤:让Agent拿生成的用例列表,回头对照PRD原文,逐段检查"这段内容是否有对应的用例覆盖",没有就标注出来。
# 覆盖率自查:用例列表 vs PRD原文,找遗漏coverage_gaps = call_llm(prompt_coverage_check, {"prd": prd_text,"test_cases": generated_cases})# 输出示例:["PRD第3段'记住登录态'无对应用例", "PRD第5段'第三方登录'无对应用例"]
这个自查步骤不是100%准确——它有时候会漏报,有时候会误报。但即使只有70%的准确率,它帮我发现的遗漏也比我自己逐条核对快得多。AI自查不需要完美,只需要比你手动检查快就够了。
第三版:让AI说出"我不知道的地方"
第二版跑了一段时间后,我发现一个规律:AI生成的用例大概能覆盖70%-80%的场景,剩下的20%-30%是它想不到的——要么是业务规则太隐蔽(比如"连续3次输错密码后锁定账号,但VIP用户锁定后可以自助解锁"),要么是跨模块联动(比如"登录失败后同时触发风控告警")。
这些场景AI想不到,因为它没有业务上下文。但它知道"这里可能有问题"——前提是你给它一个机会说出来。
我在工具里加了一个"人工补充"环节:AI生成完用例后,不是直接导出,而是先展示一个"不确定区域"列表——它标注自己在生成过程中"不确定"或"觉得可能遗漏"的地方。然后我花两分钟补充,再导出。
这个环节的关键不是补充了多少,而是让AI说出"我不确定"。一个诚实的"我不确定"比一个自信的"我覆盖了所有场景"有价值得多。
实际用下来,"不确定区域"平均每次列3-5条,其中大概2条是我确实需要补的。这2条往往是业务逻辑相关的——AI看到了PRD里的某个描述,但它不确定这个描述意味着什么测试场景,就标注出来问我。这比我自己逐段读PRD找遗漏高效得多。
完整链路长什么样
三版迭代之后,用例生成器的完整链路是这样的:
输入PRD文本 → 拆测试维度(人工确认)
搜历史用例 → 筛选相关的近20条作为参考
按维度+历史参考 → 写具体用例(超过8个测试点自动分批)
覆盖率自查 → 标注PRD中未被覆盖的段落
人工补充 → 我花2分钟补"不确定区域"
按团队模板 → 导出Excel
调API → 写入测试平台(确认后写入)
从第一版的"30秒生成一堆JSON",到第三版的"3分钟走完全链路,直接入库"。慢了两分半,但从"没人用"变成了"每周用三次以上"。
同事后来跟我说了句话,我觉得是对这个工具最好的评价:"以前你那个工具是帮你干活的,现在这个是帮我干活的。"区别在哪?第一版是从"我能做什么"出发设计的,第三版是从"她需要什么"出发设计的。
工具的价值不在于"多快生成",而在于"生成的东西能不能直接用"。
完整代码如下:
"""测试用例生成器 - 完整实现从需求文档(PRD)到结构化用例,完整链路依赖安装:pip install openai openpyxl requests使用方式:python test_case_generator.py --prd prd.txt --project "登录模块""""import jsonimport timeimport argparsefrom pathlib import Pathfrom openai import OpenAIfrom openpyxl import Workbookfrom openpyxl.styles import Font, Alignment, PatternFillimport requests# ============================================================# 配置区# ============================================================# 大模型配置LLM_API_KEY = "your-api-key-here"LLM_BASE_URL = "https://api.openai.com/v1"LLM_MODEL = "gpt-4o-mini"# 测试平台API配置PLATFORM_BASE_URL = "https://test-platform.company.com/api/v1"PLATFORM_TOKEN = "your-platform-token-here"# 批处理参数BATCH_SIZE = 6 # 超过6个测试点自动分批,6是舒适区(S4-03经验)HISTORY_CASE_LIMIT = 20 # 历史用例最多取20条# 初始化大模型客户端llm_client = OpenAI(api_key=LLM_API_KEY, base_url=LLM_BASE_URL)# ============================================================# 大模型调用封装# ============================================================def call_llm(prompt: str, input_data: str, temperature: float = 0.3) -> str:"""调用大模型,返回纯文本响应。temperature默认0.3,测试场景需要稳定性而非创造性。Args:prompt: 系统提示词/指令input_data: 输入内容(PRD文本、上一步输出等)temperature: 温度参数Returns:大模型返回的文本"""try:response = llm_client.chat.completions.create(model=LLM_MODEL,messages=[{"role": "system", "content": prompt},{"role": "user", "content": input_data}],temperature=temperature,response_format={"type": "json_object"} # 强制JSON输出)return response.choices[0].message.contentexcept Exception as e:print(f"[ERROR] LLM调用失败: {e}")raisedef call_llm_json(prompt: str, input_data: str, temperature: float = 0.3) -> dict:"""调用大模型并解析为JSON dict。自动处理大模型偶尔返回非标准JSON的情况。"""raw = call_llm(prompt, input_data, temperature)try:return json.loads(raw)except json.JSONDecodeError:# 尝试提取JSON块(大模型有时会包裹markdown代码块)import rematch = re.search(r'\{.*\}|\[.*\]', raw, re.DOTALL)if match:return json.loads(match.group())raise ValueError(f"无法解析LLM输出为JSON: {raw[:200]}")# ============================================================# 人工确认环节# ============================================================def confirm(label: str, content: str) -> bool:"""人工确认环节。打印内容,等待用户确认。这是"暂停键"——在关键步骤留人工把关。Args:label: 步骤名称content: 待确认内容Returns:True=确认通过,False=需要重跑"""print(f"\n{'='*60}")print(f"【人工确认】{label}")print(f"{'='*60}")print(content)print(f"{'='*60}")while True:choice = input("确认无误?(y=继续 / n=重跑此步 / q=退出): ").strip().lower()if choice == 'y':return Trueelif choice == 'n':return Falseelif choice == 'q':print("用户退出。")exit(0)else:print("请输入 y/n/q")# ============================================================# Prompt 模板(每一步的指令)# ============================================================# 第一步:拆测试维度PROMPT_SPLIT_DIMENSIONS = """\你是一位资深测试工程师。请分析以下需求文档,按以下五个维度拆分测试点:1. 功能验证2. 边界值3. 异常场景4. 性能关注点5. 安全风险每个维度列出3-5个测试点,测试点要具体、可测试。输出格式(JSON):{"dimensions": [{"维度": "功能验证", "测试点": ["登录成功", "登录失败", "密码错误"]},{"维度": "边界值", "测试点": ["密码最短长度", "密码最长长度"]}]}注意:- 测试点要基于需求文档内容,不要凭空捏造- 每个维度的测试点不超过5个- 如果某个维度在需求中没有相关内容,返回空数组"""# 第二步:按维度+历史参考写用例PROMPT_GENERATE_CASES = """\你是一位资深测试工程师。请根据以下测试维度和历史用例参考,编写具体的测试用例。输入数据格式(JSON):{"dimensions": [...], // 测试维度和测试点"history_cases": [...] // 同模块历史用例参考(可能为空)}每条用例必须包含以下字段:- 模块: 功能模块名称- 场景描述: 一句话说清楚测什么场景- 前置条件: 执行用例前需要满足的条件- 操作步骤: 逐步描述操作(数组,每步一个字符串)- 预期结果: 执行后应该出现的结果- 优先级: P0/P1/P2- 备注: 特殊说明(可为空字符串)输出格式(JSON):{"cases": [{"模块": "登录","场景描述": "正确的账号密码登录成功","前置条件": "账号已注册且状态正常","操作步骤": ["打开登录页面", "输入正确账号", "输入正确密码", "点击登录按钮"],"预期结果": "登录成功,跳转到首页","优先级": "P0","备注": ""}]}要求:- 参考历史用例的写法风格,但不要照搬- 每个测试点至少生成一条用例- 操作步骤要具体到可执行,不要写"执行相关操作"这种模糊描述"""# 覆盖率自查PROMPT_COVERAGE_CHECK = """\你是一位测试质量审查员。请对照需求文档原文,检查测试用例的覆盖率。输入数据(JSON):{"prd": "需求文档原文","test_cases": [生成的测试用例列表]}请逐段检查需求文档,找出"有需求描述但没有对应测试用例"的地方。输出格式(JSON):{"gaps": [{"prd段落": "需求文档中的原文片段","遗漏说明": "为什么没有覆盖到","建议": "应该补充什么用例"}],"coverage_rate": "预估覆盖率(如85%)"}注意:- 只报告确实遗漏的,不要把"已覆盖但不够细"的算作遗漏- 如果所有段落都有覆盖,返回空gaps数组"""# 不确定区域标注PROMPT_UNCERTAINTY = """\你是一位测试工程师。在刚才生成测试用例的过程中,有些地方你可能不确定。请回顾你生成的用例和需求文档,标注你"不确定"或"觉得可能遗漏"的地方。不确定的典型情况:- 需求文档描述模糊,你做了假设但不确定是否正确- 涉及业务规则,你没有足够的上下文判断- 跨模块联动,你不确定是否需要测试- 边界值不明确,你按常规处理但可能有特殊情况输出格式(JSON):{"uncertainties": [{"区域": "不确定的具体内容","原因": "为什么不确定","建议操作": "建议人工确认什么"}]}注意:- 诚实标注,不确定就说不确定- 不要为了显得全面而编造不确定项- 如果确实没有不确定的地方,返回空数组"""# ============================================================# 测试平台API封装(S4-05的封装层)# ============================================================class TestPlatformAPI:"""测试平台API封装。所有HTTP请求细节集中在这里,主流程只调用方法。换平台/换接口/换鉴权方式,只改这里。"""def __init__(self, base_url: str, token: str):self.base_url = base_url.rstrip("/")self.session = requests.Session()self.session.headers.update({"Authorization": f"Bearer {token}","Content-Type": "application/json"})def search_cases(self, module_name: str, limit: int = 20) -> list:"""搜索同模块的历史用例。用于第二步的历史参考。"""url = f"{self.base_url}/testcases/search"params = {"module": module_name,"limit": limit,"sort": "update_time:desc" # 按更新时间倒序,取最新的}resp = self.session.get(url, params=params, timeout=10)resp.raise_for_status()data = resp.json()# 实际API返回可能包了一层壳,取data字段if isinstance(data, dict) and "data" in data:return data["data"]return datadef create_case(self, case_data: dict) -> dict:"""创建单条测试用例。返回平台返回的用例信息(含用例ID)。"""url = f"{self.base_url}/testcases"resp = self.session.post(url, json=case_data, timeout=10)resp.raise_for_status()return resp.json()def batch_create_cases(self, cases: list) -> list:"""批量创建测试用例。返回每条用例的创建结果(含用例ID和平台链接)。"""results = []for i, case in enumerate(cases):try:result = self.create_case(case)results.append({"场景描述": case.get("场景描述", ""),"status": "success","case_id": result.get("data", {}).get("id", ""),"url": result.get("data", {}).get("url", "")})print(f" [{i+1}/{len(cases)}] 创建成功: {case.get('场景描述', '')}")except Exception as e:results.append({"场景描述": case.get("场景描述", ""),"status": "failed","error": str(e)})print(f" [{i+1}/{len(cases)}] 创建失败: {case.get('场景描述', '')} - {e}")# 避免请求太快被限流time.sleep(0.3)return results# ============================================================# 数据转换层(S4-05:大模型输出 → API格式)# ============================================================def transform_cases_for_platform(cases: list, project_name: str) -> list:"""将大模型输出的用例格式,转换为测试平台API要求的格式。字段映射、结构重组、默认值填充都在这里。大模型输出格式:{"模块": "...", "场景描述": "...", "操作步骤": [...], "预期结果": "...", ...}平台API格式:{"name": "...", "module": "...", "test_steps": [{"action": "...", "expected": "..."}],"expect_result": "...", "priority": "...", "project": "..."}"""transformed = []for case in cases:# 字段映射platform_case = {"name": case.get("场景描述", ""),"module": case.get("模块", ""),"project": project_name,"priority": case.get("优先级", "P2"),"precondition": case.get("前置条件", ""),"expect_result": case.get("预期结果", ""),}# 操作步骤转换:从字符串数组转为对象数组steps = case.get("操作步骤", [])if isinstance(steps, list):platform_case["test_steps"] = [{"action": step, "expected": ""} for step in steps]elif isinstance(steps, str):platform_case["test_steps"] = [{"action": steps, "expected": ""}]else:platform_case["test_steps"] = []# 必填字段校验if not platform_case["name"]:print(f" [WARN] 用例缺少场景描述,跳过: {case}")continueif not platform_case["test_steps"]:print(f" [WARN] 用例缺少操作步骤,跳过: {platform_case['name']}")continuetransformed.append(platform_case)return transformeddef transform_history_for_llm(history_cases: list) -> list:"""将平台返回的历史用例,转换为大模型参考用的精简格式。平台返回的字段很多,只取有用的,避免token浪费。"""simplified = []for case in history_cases[:HISTORY_CASE_LIMIT]:simplified.append({"场景": case.get("name", ""),"步骤": [s.get("action", "") if isinstance(s, dict) else str(s)for s in case.get("test_steps", [])],"预期": case.get("expect_result", ""),"优先级": case.get("priority", "")})return simplified# ============================================================# Excel导出模块(第二版新增)# ============================================================def export_to_excel(cases: list, output_path: str, coverage_gaps: list = None):"""按团队模板格式导出Excel。包含用例明细和覆盖率自查报告两个Sheet。"""wb = Workbook()# --- Sheet1: 测试用例 ---ws = wb.activews.title = "测试用例"# 表头样式header_font = Font(bold=True, size=11)header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")header_font_white = Font(bold=True, size=11, color="FFFFFF")headers = ["用例编号", "模块", "场景描述", "前置条件", "操作步骤","预期结果", "优先级", "备注"]for col, header in enumerate(headers, 1):cell = ws.cell(row=1, column=col, value=header)cell.font = header_font_whitecell.fill = header_fillcell.alignment = Alignment(horizontal="center", vertical="center")# 填充用例数据for idx, case in enumerate(cases, 1):steps = case.get("操作步骤", [])if isinstance(steps, list):steps_text = "\n".join(f"{i}. {s}" for i, s in enumerate(steps, 1))else:steps_text = str(steps)row_data = [f"TC-{idx:04d}",case.get("模块", ""),case.get("场景描述", ""),case.get("前置条件", ""),steps_text,case.get("预期结果", ""),case.get("优先级", "P2"),case.get("备注", "")]for col, value in enumerate(row_data, 1):cell = ws.cell(row=idx + 1, column=col, value=value)cell.alignment = Alignment(wrap_text=True, vertical="top")# 列宽设置col_widths = [12, 15, 30, 25, 40, 30, 8, 15]for col, width in enumerate(col_widths, 1):ws.column_dimensions[chr(64 + col)].width = width# --- Sheet2: 覆盖率自查报告 ---if coverage_gaps:ws2 = wb.create_sheet("覆盖率自查")gap_headers = ["PRD段落", "遗漏说明", "建议补充"]for col, header in enumerate(gap_headers, 1):cell = ws2.cell(row=1, column=col, value=header)cell.font = header_font_whitecell.fill = PatternFill(start_color="ED7D31", end_color="ED7D31", fill_type="solid")cell.alignment = Alignment(horizontal="center")for idx, gap in enumerate(coverage_gaps, 2):ws2.cell(row=idx, column=1, value=gap.get("prd段落", ""))ws2.cell(row=idx, column=2, value=gap.get("遗漏说明", ""))ws2.cell(row=idx, column=3, value=gap.get("建议", ""))for col in range(1, 4):ws2.cell(row=idx, column=col).alignment = Alignment(wrap_text=True, vertical="top")ws2.column_dimensions["A"].width = 40ws2.column_dimensions["B"].width = 35ws2.column_dimensions["C"].width = 35wb.save(output_path)print(f"\n[INFO] Excel已导出: {output_path}")# ============================================================# 分批处理逻辑(S4-03:超过BATCH_SIZE自动分批)# ============================================================def generate_cases_batched(dimensions: list, history_ref: list) -> list:"""按维度分批生成用例。超过BATCH_SIZE个测试点就分批,结果合并。防止大模型注意力衰减导致后面的测试点被"遗忘"。"""# 展开所有测试点all_points = []for dim in dimensions:for point in dim.get("测试点", []):all_points.append({"维度": dim["维度"], "测试点": point})print(f"\n[INFO] 共 {len(all_points)} 个测试点,BATCH_SIZE={BATCH_SIZE}")# 分批if len(all_points) <= BATCH_SIZE:batches = [all_points]else:batches = [all_points[i:i + BATCH_SIZE]for i in range(0, len(all_points), BATCH_SIZE)]print(f"[INFO] 测试点超过{BATCH_SIZE}个,分 {len(batches)} 批处理")# 逐批生成all_cases = []for i, batch in enumerate(batches):print(f"\n[INFO] 正在处理第 {i+1}/{len(batches)} 批...")input_data = json.dumps({"dimensions": batch,"history_cases": history_ref}, ensure_ascii=False)result = call_llm_json(PROMPT_GENERATE_CASES, input_data)batch_cases = result.get("cases", [])all_cases.extend(batch_cases)print(f"[INFO] 第 {i+1} 批生成 {len(batch_cases)} 条用例")return all_cases# ============================================================# 主流程:完整7步链路# ============================================================def generate_test_cases(prd_text: str, project_name: str,output_excel: str = None,write_to_platform: bool = False) -> list:"""测试用例生成器主流程。完整链路:1. 输入PRD → 拆测试维度(人工确认)2. 搜历史用例 → 筛选相关的作为参考3. 按维度+历史参考 → 写具体用例(自动分批)4. 覆盖率自查 → 标注PRD中未被覆盖的段落5. 人工补充 → 补"不确定区域"6. 按团队模板 → 导出Excel7. 调API → 写入测试平台(确认后写入)Args:prd_text: PRD需求文档文本project_name: 项目/模块名称output_excel: Excel输出路径(为空则不导出)write_to_platform: 是否写入测试平台Returns:生成的测试用例列表"""print(f"\n{'#'*60}")print(f"# 测试用例生成器启动")print(f"# 项目: {project_name}")print(f"# PRD长度: {len(prd_text)} 字符")print(f"{'#'*60}")# ============================================================# Step 1: 拆测试维度# ============================================================print("\n--- Step 1/7: 拆测试维度 ---")while True:result = call_llm_json(PROMPT_SPLIT_DIMENSIONS, prd_text)dimensions = result.get("dimensions", [])# 展示拆分结果display = []for dim in dimensions:points = dim.get("测试点", [])display.append(f"【{dim['维度']}】{' | '.join(points) if points else'(空)'}")ok = confirm("测试维度拆分", "\n".join(display))if ok:breakprint("[INFO] 重新拆分维度...")total_points = sum(len(d.get("测试点", [])) for d in dimensions)print(f"\n[INFO] 维度拆分完成: {len(dimensions)}个维度, {total_points}个测试点")# ============================================================# Step 2: 搜索历史用例作为参考# ============================================================print("\n--- Step 2/7: 搜索历史用例 ---")history_ref = []try:platform = TestPlatformAPI(PLATFORM_BASE_URL, PLATFORM_TOKEN)raw_history = platform.search_cases(module_name=project_name, limit=HISTORY_CASE_LIMIT)# 转换为精简格式,避免token浪费history_ref = transform_history_for_llm(raw_history)print(f"[INFO] 找到 {len(raw_history)} 条历史用例,精简后取 {len(history_ref)} 条作为参考")except Exception as e:print(f"[WARN] 获取历史用例失败(不影响主流程): {e}")print("[INFO] 将不使用历史参考,仅基于PRD生成")# ============================================================# Step 3: 按维度 + 历史参考 → 写具体用例(自动分批)# ============================================================print("\n--- Step 3/7: 生成测试用例 ---")all_cases = generate_cases_batched(dimensions, history_ref)print(f"\n[INFO] 共生成 {len(all_cases)} 条测试用例")# ============================================================# Step 4: 覆盖率自查# ============================================================print("\n--- Step 4/7: 覆盖率自查 ---")coverage_input = json.dumps({"prd": prd_text,"test_cases": all_cases}, ensure_ascii=False)coverage_result = call_llm_json(PROMPT_COVERAGE_CHECK, coverage_input)gaps = coverage_result.get("gaps", [])coverage_rate = coverage_result.get("coverage_rate", "未知")print(f"[INFO] 预估覆盖率: {coverage_rate}")if gaps:print(f"[INFO] 发现 {len(gaps)} 处遗漏:")for i, gap in enumerate(gaps, 1):print(f" {i}. PRD段落: {gap.get('prd段落', '')[:50]}...")print(f" 遗漏说明: {gap.get('遗漏说明', '')}")print(f" 建议: {gap.get('建议', '')}")else:print("[INFO] 未发现明显遗漏")# ============================================================# Step 5: 人工补充(不确定区域 + 遗漏补充)# ============================================================print("\n--- Step 5/7: 人工补充 ---")# 标注不确定区域uncertainty_input = json.dumps({"prd": prd_text,"test_cases": all_cases}, ensure_ascii=False)uncertainty_result = call_llm_json(PROMPT_UNCERTAINTY, uncertainty_input)uncertainties = uncertainty_result.get("uncertainties", [])if uncertainties:print(f"\n[AI标注的"不确定区域"](共{len(uncertainties)}处):")for i, u in enumerate(uncertainties, 1):print(f" {i}. 区域: {u.get('区域', '')}")print(f" 原因: {u.get('原因', '')}")print(f" 建议: {u.get('建议操作', '')}")print(f"\n[覆盖率遗漏](共{len(gaps)}处):")for i, gap in enumerate(gaps, 1):print(f" {i}. {gap.get('prd段落', '')[:60]}")print(f" 建议: {gap.get('建议', '')}")# 展示生成的用例供确认case_display = []for idx, case in enumerate(all_cases, 1):case_display.append(f"{idx}. [{case.get('优先级', 'P2')}] {case.get('场景描述', '')} "f"({case.get('模块', '')})")ok = confirm("用例确认(请检查后决定是否继续)","\n".join(case_display) + f"\n\n覆盖率: {coverage_rate}"f"\n遗漏: {len(gaps)}处 | 不确定: {len(uncertainties)}处")if not ok:print("[INFO] 用例需要调整,重新生成...")# 这里可以选择重新生成或让用户手动编辑后继续# 简化处理:直接退出,用户修改PRD后重跑print("[提示] 如需调整,请修改PRD后重新运行,或手动编辑生成的用例")return all_cases# ============================================================# Step 6: 导出Excel# ============================================================if output_excel:print(f"\n--- Step 6/7: 导出Excel ---")export_to_excel(all_cases, output_excel, gaps)# ============================================================# Step 7: 写入测试平台(确认后写入)# ============================================================if write_to_platform:print("\n--- Step 7/7: 写入测试平台 ---")ok = confirm("写入测试平台",f"即将写入 {len(all_cases)} 条用例到测试平台\n"f"项目: {project_name}\n"f"平台: {PLATFORM_BASE_URL}\n\n"f"确认写入?")if not ok:print("[INFO] 跳过写入平台,用例已导出到本地")return all_cases# 数据转换:大模型格式 → 平台API格式platform_cases = transform_cases_for_platform(all_cases, project_name)print(f"[INFO] 数据转换完成: {len(platform_cases)} 条用例待写入")# 批量写入platform = TestPlatformAPI(PLATFORM_BASE_URL, PLATFORM_TOKEN)results = platform.batch_create_cases(platform_cases)# 统计结果success_count = sum(1 for r in results if r["status"] == "success")failed_count = len(results) - success_countprint(f"\n[结果] 写入完成: 成功 {success_count} 条, 失败 {failed_count} 条")# 打印平台链接for r in results:if r["status"] == "success" and r.get("url"):print(f" - {r['场景描述']}: {r['url']}")print(f"\n{'#'*60}")print(f"# 测试用例生成完成!")print(f"# 共 {len(all_cases)} 条用例")print(f"# 覆盖率: {coverage_rate}")if output_excel:print(f"# Excel: {output_excel}")print(f"{'#'*60}")return all_cases# ============================================================# 命令行入口# ============================================================def main():parser = argparse.ArgumentParser(description="测试用例生成器")parser.add_argument("--prd", required=True, help="PRD文件路径")parser.add_argument("--project", required=True, help="项目/模块名称")parser.add_argument("--excel", default=None, help="Excel输出路径(不指定则不导出)")parser.add_argument("--platform", action="store_true", help="是否写入测试平台")args = parser.parse_args()# 读取PRDprd_path = Path(args.prd)if not prd_path.exists():print(f"[ERROR] PRD文件不存在: {prd_path}")returnprd_text = prd_path.read_text(encoding="utf-8")if not prd_text.strip():print("[ERROR] PRD文件为空")return# 默认Excel输出路径excel_path = args.excel or f"测试用例_{args.project}_{int(time.time())}.xlsx"# 运行generate_test_cases(prd_text=prd_text,project_name=args.project,output_excel=excel_path,write_to_platform=args.platform)if __name__ == "__main__":main()
一句话总结:造用例生成器最大的坑不是技术跑不通,是"技术上跑通了但没人用"——补上团队模板、历史参考、覆盖率自查、人工补充四个环节,工具从"能生成"变成"能用"。AI自查不需要完美,比你手动检查快就够了。
夜雨聆风