Dify 高级实验(09):测试用例生成——如何让 AI 自动产出测试用例?
1. 业务场景
先讲一个我们实际遇到的场景。
一家软件公司的 QA 团队,每次版本迭代都要为新增功能写测试用例。产品经理丢过来一段功能描述:「用户注册功能,输入手机号验证码注册」——测试同学就要手工拆出正常、边界、异常一堆场景,写成 Excel 用例表,一个功能少则几小时,多则一两天。赶上版本密集期,测试用例写得仓促,边界场景漏了,异常路径没覆盖,上线后 bug 全从漏掉的用例里冒出来。测试组长最头疼的是:写用例这种「经验活」,偏偏最费时间,还总被压缩。
我们第一次接到这个需求时,第一反应也是「把用例模板标准化,提高复用率」。后来才发现——拆测试点这件事,模式化程度高得惊人:正常/边界/异常三类、P0-P3 分级、前置条件/步骤/预期结果三件套,全是固定套路。而「固定套路 + 大量枚举」正是 LLM 最擅长的活,人做反而又慢又漏。
这不是个例。任何「把功能描述变成测试用例」的团队都是这个模式:Web 功能测试、App 版本验收、接口回归——测试点拆分(正常/边界/异常)、优先级分级(P0-P3)、用例表格化,这套流程高度模式化,却又完全依赖人工逐条产出。
2. 场景痛点
这个流程的痛点,在 QA 团队身上体现得最直接:
- 写用例耗时长:一个功能从拆测试点到写成 Excel 用例表,半天起步——版本迭代越快,用例积压越严重,测试成了上线瓶颈。
- 边界和异常场景靠人记:空输入、超长输入、重复提交、并发——这些场景漏一个,上线就多一个隐患,而人脑记不全「所有可能出错的地方」。
- 优先级不分级:用例一锅烩,P0 核心功能和 P3 边缘场景混在一起——回归时不知道该先跑哪些,测试资源分配全凭感觉。
- 格式不统一:每个测试同学写用例的字段、措辞都不一样,用例表没法直接复用,换人接手还要先「翻译」上一版格式。
本质上,写测试用例的瓶颈不是「想不出场景」,而是「模式化产出全靠手工」——拆解、分级、格式化都是确定性流程,恰恰是 LLM 最擅长、最该自动化的部分。
3. 方案:为什么是这套生成式工作流
Dify 工作流里的「Agent 编排 + 结构化输出」链路,让 LLM 扮演 QA 工程师,从一段功能描述自动产出可直接执行的测试用例。
选它的理由,我们实际对比过:
- LLM 拆测试点又快又全:让模型按「功能/边界/异常」三类拆测试点并分级 P0-P3,覆盖度比人脑枚举更全——它见过的系统足够多,知道「哪里容易出错」;
- 迭代节点逐条展开:每个测试点用迭代节点生成完整用例(前置条件/步骤/预期结果),一条用例一个 JSON,结构稳定可校验;
- CSV 交付零改造:代码节点
csv.writer汇总成 CSV,直接复制进 Excel——输出格式贴合团队现有习惯,落地不用改流程。
这篇文章我们就用它搭一个「测试用例生成器」:输入功能描述,自动产出 10-15 条带优先级的完整用例,直接复制进 Excel。
4. 整体架构
链路很清晰:入口收功能描述 → 生成测试点 → 解析兜底 → 逐条生成用例 → 汇总 CSV → 格式化交付。三层结构化输出是这个架构的关键——测试点是 JSON 数组、单条用例是 JSON、交付物是 CSV,每一层都可被代码节点稳定解析和校验。
5. 模块设计
5.1 开始节点
- label: 功能描述
required: true
type: paragraph
variable: feature_desc
- label: 模块名称
required: true
type: text-input
variable: module_name5.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. 实验文档及源码获取
- 实验文档(完整操作步骤):DIFY-103-09:测试用例生成器.md
- 源码(可直接导入,知识库已预填 dataset_id):dify103_09_测试用例生成器.yml
文章聚焦核心配置与采坑点;实验的完整分步操作(知识库种子准备/节点搭建/参数表)见实验文档原文。
- Dify 高级实验(01):智能文档处理——如何把非结构化文档变成结构化数据?
- Dify 高级实验(02):对话式 BI 分析——如何让业务人员用自然语言直接查数?
- Dify 高级实验(03):智能客服——如何让 AI 自动应答并把疑难问题转人工?
- Dify 高级实验(04):RAG 增强问答——如何让知识库问答更准还能多轮追问?
- Dify 高级实验(05):多步推理——如何把复杂问题拆解成子问题逐个击破?
- Dify 高级实验(06):自动化报告——如何让系统定时自动生成周报?
- Dify 高级实验(07):智能审批——如何让机器自动完成流程审批?
- Dify 高级实验(08):智能告警——如何用 AI 替代固定阈值监控?
- Dify 高级实验(09):测试用例生成——如何让 AI 自动产出测试用例?
- Dify 高级实验(10):综合实战——如何从零搭建一个企业级智能客服平台?