Dify 高级实验(09):测试用例生成------如何让 AI 自动产出测试用例?
Dify 实验系列 · 高级 09/10 | 实验编号:DIFY-103-09
1. 实验目的
掌握「Agent 编排 + 结构化输出」的生成式工作流范式:让 LLM 扮演 QA 工程师,从一段功能描述自动产出可直接执行的测试用例(CSV 格式)。核心是三层结构化输出链路:
- 测试点列表:LLM 输出 JSON 数组(类型/描述/优先级),代码节点解析兜底;
- 逐条用例:迭代节点对每个测试点生成完整用例 JSON(前置条件/步骤/预期结果);
- CSV 交付 :代码节点
csv.writer汇总,LLM 最终格式化出统计摘要 + 完整 CSV。
适合:QA / 测试开发 / 需要把「写用例」自动化的团队。前置要求:中级实验 09 代码节点、14 知识库检索、16 迭代节点。
2. 场景设计
输入:feature_desc(功能描述,paragraph)+ module_name(模块名称,text-input);输出:一份含统计摘要与完整 CSV 的测试用例文档,可直接复制进 Excel。
输入「用户注册功能,输入手机号验证码注册」→ 自动产出 10-15 条用例(正常/边界/异常全覆盖,P0-P3 分级)------QA 半天的手工工作量压缩到几分钟。
3. 节点拓扑
text
开始(feature_desc + module_name)
→ 知识库检索(测试用例模板,top_k=3)
→ LLM 生成测试点(JSON 数组:类型/描述/优先级)
→ Code 解析测试点(json.loads + 截断 25 条 + 注入 module)
→ 迭代节点(逐条生成详细用例)
└→ 迭代内 LLM 生成单条用例(JSON:前置条件/步骤/预期)
→ Code 汇总 CSV(csv.writer)
→ LLM 格式化输出(统计摘要 + 完整 CSV)→ 结束
4. 关键配置
4.1 开始节点
yaml
- label: 功能描述
required: true
type: paragraph
variable: feature_desc
- label: 模块名称
required: true
type: text-input
variable: module_name
4.2 生成测试点(lm_points)
数量约束 + 「只输出 JSON」+ 结构示例------让输出可被下游代码节点稳定解析:
jinja2
你是一个 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。
4.3 解析测试点(cd_parse)
两个关键设计:① 迭代数组上限 30,points[:25] 截断留余量;② 把 module 注入每个 item------迭代内 LLM 只引用 item,不引用外层节点(自包含模式,依赖只进不出):
python
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)}
4.4 迭代内生成单条用例(lm_case)
迭代容器输入 [cd_parse, test_points],输出收集 [lm_case, text](string 是迭代 output_selector 可见类型)。内部 prompt 全部从 item 取字段,同样 reasoning_format: separated:
jinja2
根据测试点生成完整的测试用例。
模块:{{#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": "预期结果"}
要求:前置条件具体、步骤可执行、预期结果可验证。
4.5 汇总 CSV(cd_summary)
迭代输出收集到的是 array[string],每条可能是字符串------先 json.loads 还原再写 CSV:
python
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)}
4.6 格式化输出(lm_format)
max_tokens: 8000 + 明确「只输出正文」------长交付物场景的标配双保险:
jinja2
你是 QA 测试用例交付专员。基于以下 CSV 内容生成一份可直接交付的测试用例文档。
共生成 {{#cd_summary.case_count#}} 条测试用例,CSV 内容如下:
{{#cd_summary.csv_output#}}
请输出:1. 测试用例统计摘要(总数、功能/边界/异常分布)2. 完整 CSV 内容(保持原样,方便复制导入 Excel)。
只输出最终报告正文,不要输出任何解释性文字、思考过程或 Markdown 代码块标记。
5. 运行验证
| 输入(feature_desc) | 预期 | 实测 |
|---|---|---|
| 用户注册功能,输入手机号验证码注册 | 10-15 条用例,覆盖功能/边界/异常,输出统计摘要 + CSV | 与预期一致 |
| 购物车结算功能,支持优惠券 | 15-20 条用例,优先级 P0-P3 分级 | 与预期一致 |
| 空功能描述 / 超长描述 | 不崩溃,输出空用例文档或少量兜底用例 | 与预期一致(cd_parse 空数组兜底) |
6. 采坑点
| 坑 | 现象 | 修复 |
|---|---|---|
| 迭代链路 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 约束即恢复) |
7. 实验文档及源码获取
- 实验文档 (完整操作步骤):DIFY-103-09:测试用例生成器.md
- 源码 (可直接导入,知识库已预填 dataset_id):dify103_09_测试用例生成器.yml
文章聚焦核心配置与采坑点;实验的完整分步操作(知识库种子准备/节点搭建/参数表)见实验文档原文。
下一篇:Dify 高级实验(10):综合实战------如何从零搭建一个企业级智能客服平台?
💬 你在这个实验的场景里踩过什么坑?欢迎评论区分享你的实战经验。