← 返回文章列表

Dify 高级实验(09):测试用例生成——如何让 AI 自动产出测试用例?

1. 业务场景

先讲一个我们实际遇到的场景。

一家软件公司的 QA 团队,每次版本迭代都要为新增功能写测试用例。产品经理丢过来一段功能描述:「用户注册功能,输入手机号验证码注册」——测试同学就要手工拆出正常、边界、异常一堆场景,写成 Excel 用例表,一个功能少则几小时,多则一两天。赶上版本密集期,测试用例写得仓促,边界场景漏了,异常路径没覆盖,上线后 bug 全从漏掉的用例里冒出来。测试组长最头疼的是:写用例这种「经验活」,偏偏最费时间,还总被压缩

我们第一次接到这个需求时,第一反应也是「把用例模板标准化,提高复用率」。后来才发现——拆测试点这件事,模式化程度高得惊人:正常/边界/异常三类、P0-P3 分级、前置条件/步骤/预期结果三件套,全是固定套路。而「固定套路 + 大量枚举」正是 LLM 最擅长的活,人做反而又慢又漏。

这不是个例。任何「把功能描述变成测试用例」的团队都是这个模式:Web 功能测试、App 版本验收、接口回归——测试点拆分(正常/边界/异常)、优先级分级(P0-P3)、用例表格化,这套流程高度模式化,却又完全依赖人工逐条产出。

2. 场景痛点

这个流程的痛点,在 QA 团队身上体现得最直接:

本质上,写测试用例的瓶颈不是「想不出场景」,而是「模式化产出全靠手工」——拆解、分级、格式化都是确定性流程,恰恰是 LLM 最擅长、最该自动化的部分。

3. 方案:为什么是这套生成式工作流

Dify 工作流里的「Agent 编排 + 结构化输出」链路,让 LLM 扮演 QA 工程师,从一段功能描述自动产出可直接执行的测试用例。

选它的理由,我们实际对比过:

这篇文章我们就用它搭一个「测试用例生成器」:输入功能描述,自动产出 10-15 条带优先级的完整用例,直接复制进 Excel。

4. 整体架构

graph TD start["开始:feature_desc + module_name"] --> kb["知识库检索:测试用例模板,top_k=3"] kb --> lp["LLM 生成测试点:JSON 数组(类型/描述/优先级)"] lp --> cp["Code 解析测试点:json.loads + 截断 25 条 + 注入 module"] cp --> it["迭代节点:逐条生成详细用例"] it --> lc["迭代内 LLM 生成单条用例:JSON(前置条件/步骤/预期)"] it --> cs["Code 汇总 CSV:csv.writer"] cs --> lf["LLM 格式化输出:统计摘要 + 完整 CSV"] lf --> e1["结束"]

链路很清晰:入口收功能描述 → 生成测试点 → 解析兜底 → 逐条生成用例 → 汇总 CSV → 格式化交付。三层结构化输出是这个架构的关键——测试点是 JSON 数组、单条用例是 JSON、交付物是 CSV,每一层都可被代码节点稳定解析和校验。

5. 模块设计

5.1 开始节点

- label: 功能描述

  required: true

  type: paragraph

  variable: feature_desc

- label: 模块名称

  required: true

  type: text-input

  variable: module_name

5.2 生成测试点(lm_points)

数量约束 + 「只输出 JSON」+ 结构示例——让输出可被下游代码节点稳定解析:

你是一个 QA 工程师。根据功能描述生成测试点列表。

功能描述:{{#start.feature_desc#}}

模块:{{#start.module_name#}}

参考模板:{{#kb_templates.result#}}

要求:

1. 覆盖功能测试、边界测试、异常测试

2. 每个测试点包含:类型(功能/边界/异常)、描述、优先级(P0-P3)

3. P0 是核心功能必须覆盖,P3 是边缘场景

4. 测试点数量控制在 10-15 个

5. 输出 JSON 数组格式(只输出 JSON,不要输出其他任何内容)

输出格式:[{"type": "功能", "description": "正常登录", "priority": "P0"}, ...]

注意该节点必须 reasoning_format: separated——思考过程一旦混入 text,下方 cd_parse 的 json.loads 直接失败,流程卡在步骤 0。

5.3 解析测试点(cd_parse)

两个关键设计:① 迭代数组上限 30,points[:25] 截断留余量;② 把 module 注入每个 item——迭代内 LLM 只引用 item,不引用外层节点(自包含模式,依赖只进不出):

def main(llm_text: str, module_name: str) -> dict:

    import json, re

    text = (llm_text or "").strip()

    points = []

    m = re.search(r"\[.*\]", text, re.DOTALL)

    if m:

        try:

            parsed = json.loads(m.group(0))

            if isinstance(parsed, list):

                points = parsed

        except Exception:

            points = []

    points = points[:25]  # 迭代数组上限 30,截断留余量

    items = []

    for p in points:

        if not isinstance(p, dict):

            continue

        items.append({

            "type": str(p.get("type", "功能")),

            "description": str(p.get("description", "")),

            "priority": str(p.get("priority", "P2")),

            "module": str(module_name or "未指定模块")

        })

    return {"test_points": items, "count": len(items)}

5.4 迭代内生成单条用例(lm_case)

迭代容器输入 [cd_parse, test_points],输出收集 [lm_case, text](string 是迭代 output_selector 可见类型)。内部 prompt 全部从 item 取字段,同样 reasoning_format: separated

根据测试点生成完整的测试用例。

模块:{{#iter_cases.item.module#}}

测试点:{{#iter_cases.item.description#}}

类型:{{#iter_cases.item.type#}}

优先级:{{#iter_cases.item.priority#}}

输出 JSON(只输出 JSON,不要输出其他任何内容):

{"module": "模块名", "test_point": "测试点描述", "type": "功能/边界/异常",

 "priority": "P0-P3", "precondition": "前置条件", "steps": ["步骤1", "步骤2"],

 "expected": "预期结果"}

要求:前置条件具体、步骤可执行、预期结果可验证。

5.5 汇总 CSV(cd_summary)

迭代输出收集到的是 array[string],每条可能是字符串——先 json.loads 还原再写 CSV:

def main(all_cases: list) -> dict:

    import csv, io, json

    cases = []

    for c in all_cases or []:

        if isinstance(c, str):

            try:

                c = json.loads(c)

            except Exception:

                c = {}

        if isinstance(c, dict):

            cases.append(c)

    output = io.StringIO()

    writer = csv.writer(output)

    writer.writerow(["模块", "测试点", "类型", "优先级", "前置条件", "步骤", "预期结果"])

    for case in cases:

        steps = case.get("steps", []) or []

        if not isinstance(steps, list):

            steps = [str(steps)]

        writer.writerow([case.get("module", ""), case.get("test_point", ""),

            case.get("type", ""), case.get("priority", ""),

            case.get("precondition", ""), "; ".join(str(s) for s in steps),

            case.get("expected", "")])

    return {"csv_output": output.getvalue(), "case_count": len(cases)}

5.6 格式化输出(lm_format)

max_tokens: 8000 + 明确「只输出正文」——长交付物场景的标配双保险:

你是 QA 测试用例交付专员。基于以下 CSV 内容生成一份可直接交付的测试用例文档。

共生成 {{#cd_summary.case_count#}} 条测试用例,CSV 内容如下:

{{#cd_summary.csv_output#}}

请输出:1. 测试用例统计摘要(总数、功能/边界/异常分布)2. 完整 CSV 内容(保持原样,方便复制导入 Excel)。

只输出最终报告正文,不要输出任何解释性文字、思考过程或 Markdown 代码块标记。

6. 运行验证

输入(feature_desc) 预期 实测
用户注册功能,输入手机号验证码注册 10-15 条用例,覆盖功能/边界/异常,输出统计摘要 + CSV 与预期一致
购物车结算功能,支持优惠券 15-20 条用例,优先级 P0-P3 分级 与预期一致
空功能描述 / 超长描述 不崩溃,输出空用例文档或少量兜底用例 与预期一致(cd_parse 空数组兜底)

7. 实战坑

现象 修复
迭代链路 LLM 思考污染 lm_points 未开推理标签分离时,text 混入 <think> 思考,cd_parse 的 json.loads 解析失败,流程卡在步骤 0 lm_points / lm_case 显式 reasoning_format: separated(DeepSeek 推理模型默认 tagged,必须显式写)
迭代输入数组超过 30 个元素 运行报 length of var "item" must be less than 30 elements 双保险:prompt 限「10-15 个」+ cd_parse points[:25] 截断留余量(dify103_09 P3 实测)
迭代内 LLM 引用外层节点字段 上下文依赖外传、字段取不到,调试困难 自包含模式:进入迭代前把 module 等上下文注入每个 item,迭代内只引用 {{#iter_cases.item.xxx#}}
迭代输出每条是字符串,直接当 dict 用报错 迭代 output_selector 只能收集 string,对 str 调 case.get("module") 抛 AttributeError cd_summary 先 isinstance(c, str)json.loads 还原
长交付文档 text 为空 思考与输出共享 max_tokens(默认 2000)被挤空,或内容写进思考部分 lm_format max_tokens: 8000 + prompt「只输出最终报告正文」(dify103_09 实测:只加 prompt 约束即恢复)

8. 实验文档及源码获取

文章聚焦核心配置与采坑点;实验的完整分步操作(知识库种子准备/节点搭建/参数表)见实验文档原文。

联系我

15088711270

手机端点击号码可直接拨打 · 桌面端可复制

微信二维码

扫码加微信 · 备注「门户」更快通过