目前我们的测试用例主要依赖人工生成和维护,AI时代的来临,我们也在思考“AI如何赋能业务”,提出了如下命题:
“探索通过AI辅助生成测试用例,完成从需求到测试用例生成的穿刺”。
需求名称:注册登录流程 需求描述: 1、注册和登录在同一个页面,有2个按钮,一个注册,一个登录,用户输入用户名、密码进行登录或者注册 2、首页:加载一张图,有个退出按钮,点击则退出首页
注:这里只是为了验证思路,需求描述会比较简单,实际需求考虑会更完善。
使用Dify[1] 搭建的工作流:
将前面的需求描述作为输入参数,提供Prompt模板告诉LLM,如下所示: [2]
LLM 生成的mermaid 状态机描述:
stateDiagram-v2
[*] --> Unregistered
Unregistered --> Registering: start_register
Registering --> Unregistered: register_failed
Registering --> LoggingIn: register_success
Unregistered --> LoggingIn: start_login
LoggingIn --> Unregistered: login_failed
LoggingIn --> LoggedIn: login_success
LoggedIn --> Unregistered: logout
LoggedIn --> [*]: exit
Markdown对mermaid支持友好,可以直接渲染成状态机图:
这里选择Mermaid来描述状态机的理由,主要是Mermaid天然适合文档化,代码轻量且无额外依赖,无需处理图片格式的一些问题。
参考:AI大模型生成的图表为什么倾向使用Mermaid格式?[3]
前面通过LLM能够帮我们理解需求生成状态机图,如果想基于状态机找全测试路径,我们尝试使用AI编程工具来辅助生成规则工具,来确保每次遍历的路径是一致的。
比如Cursor:
通过多轮的对话和人工修正,Cursor能够很高效的帮助我生成符合预期的代码,但仍需要人工去验证和调试。
核心路径生成算法:
from typing importList, Dict, Set
from abc import ABC, abstractmethod
classPathGeneratorBase(ABC):
def__init__(self):
self.graph = {}
self.paths = []
self.events = {}
@abstractmethod
defparse_input(self):
"""解析输入源(Mermaid或SCXML)"""
pass
defgenerate_paths(self, max_depth: int = ) -> List[List[str]]:
"""通用的路径生成算法"""
paths = []
start = self._find_start_state()
visited_states = set()
defdfs(current: str, path: List[str]):
iflen(path) > max_depth:
return
current_transitions = self._get_transitions(current)
ifself._should_terminate(current, path, current_transitions):
paths.append(path[:])
return
visited_states.add(current)
for next_state in current_transitions:
dfs(next_state, path + [next_state])
visited_states.remove(current)
dfs(start, [start])
returnself._deduplicate_paths(paths)
def_find_start_state(self) -> str:
"""查找起始状态"""
if'START'inself.graph:
return'START'
in_degrees = self._calculate_in_degrees()
for node, degree in in_degrees.items():
if degree == :
return node
returnNone
def_get_transitions(self, state: str) -> List[str]:
"""获取状态的所有可能转换"""
if state notinself.graph:
return []
return [target for target inself.graph[state]]
def_should_terminate(self, current: str, path: List[str], transitions: List[str]) -> bool:
"""判断是否应该终止当前路径"""
returnlen(path) > and (not transitions or current in path[:-])
def_deduplicate_paths(self, paths: List[List[str]]) -> List[List[str]]:
"""去除重复路径"""
unique_paths = []
path_strings = set()
for path insorted(paths, key=len):
path_str = "->".join(path)
if path_str notin path_strings:
path_strings.add(path_str)
unique_paths.append(path)
return unique_paths
defcalculate_coverage(self) -> Dict:
"""计算测试覆盖率"""
all_states = set(self.graph.keys())
all_transitions = set()
covered_states = set()
covered_transitions = set()
for path inself.paths:
covered_states.update(path)
for i inrange(len(path) - ):
transition = (path[i], path[i + ])
covered_transitions.add(transition)
all_transitions.add(transition)
return {
"state_coverage": len(covered_states) / len(all_states) * ,
"transition_coverage": len(covered_transitions) / len(all_transitions) *
}
根据路径生成算法遍历生成的路径索引:
示例:
场景 | 前置条件 | 操作步骤 | 预期结果 |
---|---|---|---|
银行卡支付场景 | 用户已绑定有效银行卡 | 在收银台选择银行卡支付 | 显示支付成功页面,账户余额减少相应金额 |
工作流设计:
生成的测试用例结果示例:
[
{
"precondition":"用户未注册,系统处于未登录状态",
"operation_procedure":[
"1. 访问系统首页",
"2. 输入用户名:a1_(3字符)",
"3. 输入密码:Passw0rd!",
"4. 点击'注册'按钮"
],
"expected_result":[
"注册失败,提示'用户名长度需在4-20字符之间'",
"页面停留在注册/登录页面"
]
},
{
"precondition":"用户未注册,系统处于未登录状态",
"operation_procedure":[
"1. 访问系统首页",
"2. 输入用户名:existing_user",
"3. 输入密码:Passw0rd!",
"4. 点击'注册'按钮",
"5. 再次输入相同用户名和密码点击'注册'按钮"
],
"expected_result":[
"第二次注册时提示'用户名已存在'",
"页面停留在注册/登录页面"
]
},
{
"precondition":"用户未注册,系统处于未登录状态",
"operation_procedure":[
"1. 访问系统首页",
"2. 输入用户名:user😊name",
"3. 输入密码:Passw0rd!",
"4. 点击'注册'按钮"
],
"expected_result":[
"注册失败,提示'用户名包含非法字符'",
"页面停留在注册/登录页面"
]
},
{
"precondition":"用户未注册,系统处于未登录状态",
"operation_procedure":[
"1. 访问系统首页",
"2. 输入不存在的用户名:new_user",
"3. 输入错误密码:wrongPass",
"4. 点击'登录'按钮"
],
"expected_result":[
"登录失败,提示'用户名或密码错误'",
"页面停留在注册/登录页面"
]
},
{
"precondition":"用户已注册,系统处于未登录状态",
"operation_procedure":[
"1. 访问系统首页",
"2. 输入已注册用户名:valid_user",
"3. 输入正确密码:Passw0rd!",
"4. 点击'登录'按钮"
],
"expected_result":[
"登录成功,跳转至带图片的首页",
"页面显示退出按钮"
]
},
{
"precondition":"新用户完成注册,系统处于未登录状态",
"operation_procedure":[
"1. 访问系统首页",
"2. 输入符合要求的用户名:new_user_123",
"3. 输入符合要求的密码:Passw0rd!",
"4. 点击'注册'按钮",
"5. 使用相同凭证点击'登录'按钮"
],
"expected_result":[
"注册成功后自动清除表单",
"登录成功跳转至首页"
]
},
{
"precondition":"用户已登录,处于首页",
"operation_procedure":[
"1. 点击'退出'按钮"
],
"expected_result":[
"返回注册/登录页面",
"用户会话终止"
]
},
{
"precondition":"用户未注册,系统处于未登录状态",
"operation_procedure":[
"1. 访问系统首页",
"2. 输入有效用户名:valid_user",
"3. 输入无效密码:password",
"4. 点击'注册'按钮"
],
"expected_result":[
"注册失败,提示'密码需包含大写字母、小写字母、数字和特殊字符'",
"页面停留在注册/登录页面"
]
}
]
这里可以进一步将json数据转成xmind或excel格式的文件。
价值思考? 几个方向:
[1]
Dify: https://docs.dify.ai/zh-hans
[2]
: https://cdn.nlark.com/yuque/0/2025/png/1214279/1746627272185-d8f5b7c5-666f-4686-8985-77eeaf0a6b43.png
[3]
AI大模型生成的图表为什么倾向使用Mermaid格式?: https://juejin.cn/post/7475617896543338511