乐于分享
好东西不私藏

造一个测试用例生成器:从需求文档到结构化用例(附完整代码)

造一个测试用例生成器:从需求文档到结构化用例(附完整代码)

上篇结尾说了,从这篇开始进入实战板块——把前面三篇讲的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找遗漏高效得多。

完整链路长什么样

三版迭代之后,用例生成器的完整链路是这样的:

  1. 输入PRD文本 → 拆测试维度(人工确认)

  2. 搜历史用例 → 筛选相关的近20条作为参考

  3. 按维度+历史参考 → 写具体用例(超过8个测试点自动分批)

  4. 覆盖率自查 → 标注PRD中未被覆盖的段落

  5. 人工补充 → 我花2分钟补"不确定区域"

  6. 按团队模板 → 导出Excel

  7. 调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.content    except 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 re        match = 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 True        elif choice == 'n':            return False        elif 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 data    def 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}")            continue        if not platform_case["test_steps"]:            print(f"  [WARN] 用例缺少操作步骤,跳过: {platform_case['name']}")            continue        transformed.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.active    ws.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_white        cell.fill = header_fill        cell.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_white            cell.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 = 40        ws2.column_dimensions["B"].width = 35        ws2.column_dimensions["C"].width = 35    wb.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. 按团队模板 → 导出Excel    7. 调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:            break        print("[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_count        print(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()    # 读取PRD    prd_path = Path(args.prd)    if not prd_path.exists():        print(f"[ERROR] PRD文件不存在: {prd_path}")        return    prd_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自查不需要完美,比你手动检查快就够了。

相关学习资料