AI 实践探索:辅助生成测试用例

背景

目前我们的测试用例主要依赖人工生成和维护,AI时代的来临,我们也在思考"AI如何赋能业务",提出了如下命题:

"探索通过AI辅助生成测试用例,完成从需求到测试用例生成的穿刺"。

目标

  • 找全测试路径
  • 辅助生成测试用例

实践案例:登录注册流程

自然语言描述需求

需求名称:注册登录流程

需求描述:

1、注册和登录在同一个页面,有2个按钮,一个注册,一个登录,用户输入用户名、密码进行登录或者注册

2、首页:加载一张图,有个退出按钮,点击则退出首页

注:这里只是为了验证思路,需求描述会比较简单,实际需求考虑会更完善。

如何找全测试路径

使用LLM生成mermaid格式的状态机描述

使用Dify 搭建的工作流:

将前面的需求描述作为输入参数,提供Prompt模板 告诉LLM,如下所示:

LLM 生成的mermaid 状态机描述:

plain 复制代码
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格式?

使用AI帮我们开发工具

前面通过LLM能够帮我们理解需求生成状态机图,如果想基于状态机找全测试路径,我们尝试使用AI编程工具来辅助生成规则工具,来确保每次遍历的路径是一致的。

比如Cursor:

通过多轮的对话和人工修正,Cursor能够很高效的帮助我生成符合预期的代码,但仍需要人工去验证和调试。

核心路径生成算法:

python 复制代码
from typing import List, Dict, Set
from abc import ABC, abstractmethod

class PathGeneratorBase(ABC):
    def __init__(self):
        self.graph = {}
        self.paths = []
        self.events = {}
        
    @abstractmethod
    def parse_input(self):
        """解析输入源(Mermaid或SCXML)"""
        pass
        
    def generate_paths(self, max_depth: int = 15) -> List[List[str]]:
        """通用的路径生成算法"""
        paths = []
        start = self._find_start_state()
        visited_states = set()
        
        def dfs(current: str, path: List[str]):
            if len(path) > max_depth:
                return
                
            current_transitions = self._get_transitions(current)
            
            if self._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])
        return self._deduplicate_paths(paths)
        
    def _find_start_state(self) -> str:
        """查找起始状态"""
        if 'START' in self.graph:
            return 'START'
            
        in_degrees = self._calculate_in_degrees()
        for node, degree in in_degrees.items():
            if degree == 0:
                return node
        return None
        
    def _get_transitions(self, state: str) -> List[str]:
        """获取状态的所有可能转换"""
        if state not in self.graph:
            return []
        return [target for target in self.graph[state]]
        
    def _should_terminate(self, current: str, path: List[str], transitions: List[str]) -> bool:
        """判断是否应该终止当前路径"""
        return len(path) > 1 and (not transitions or current in path[:-1])
        
    def _deduplicate_paths(self, paths: List[List[str]]) -> List[List[str]]:
        """去除重复路径"""
        unique_paths = []
        path_strings = set()
        
        for path in sorted(paths, key=len):
            path_str = "->".join(path)
            if path_str not in path_strings:
                path_strings.add(path_str)
                unique_paths.append(path)
                
        return unique_paths
        
    def calculate_coverage(self) -> Dict:
        """计算测试覆盖率"""
        all_states = set(self.graph.keys())
        all_transitions = set()
        covered_states = set()
        covered_transitions = set()
        
        for path in self.paths:
            covered_states.update(path)
            for i in range(len(path) - 1):
                transition = (path[i], path[i + 1])
                covered_transitions.add(transition)
                all_transitions.add(transition)
                
        return {
            "state_coverage": len(covered_states) / len(all_states) * 100,
            "transition_coverage": len(covered_transitions) / len(all_transitions) * 100
        }

根据路径生成算法遍历生成的路径索引:

如何生成测试用例

用例关键要素

  • 前置条件:描述复现测试场景所需的条件;
  • 操作步骤:描述测试场景下用户的操作行为;
  • 预期结果:描述测试场景下,用户执行完操作预期得到的结果。

示例:

场景 前置条件 操作步骤 预期结果
银行卡支付场景 用户已绑定有效银行卡 在收银台选择银行卡支付 显示支付成功页面,账户余额减少相应金额

LLM 生成测试用例

工作流设计:

  1. 输入需求描述和测试路径
  2. 设计prompt提示词,要求LLM按固定格式输出测试用例

生成的测试用例结果示例:

json 复制代码
[
  {
    "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格式的文件。

收益

价值思考? 几个方向:

  • AI辅助建模(状态机、类图),提升研发需求建模效率和规范
  • 释放助理生成测试用例的生产力,转变为review/修改用例的角色
  • AI辅助生成符合业务规则的可测试数据,提升构造数据的效率

经验总结

  • 使用AI帮助我们进行自然语言推理和图解析
  • 要让AI生成的测试用例更准确,需要精确描述需求,对需求进行建模,描述清楚业务规则
  • 使用AI编程工具帮助我们开发工具,比如有明确规则的工具开发
  • 通过搭建AI工作流完成需求穿刺

附录

相关推荐
陈广亮16 小时前
构建具有长期记忆的 AI Agent:从设计模式到生产实践
人工智能
会写代码的柯基犬17 小时前
DeepSeek vs Kimi vs Qwen —— AI 生成俄罗斯方块代码效果横评
人工智能·llm
Mintopia17 小时前
OpenClaw 是什么?为什么节后热度如此之高?
人工智能
爱可生开源社区17 小时前
DBA 的未来?八位行业先锋的年度圆桌讨论
人工智能·dba
叁两20 小时前
用opencode打造全自动公众号写作流水线,AI 代笔太香了!
前端·人工智能·agent
前端付豪20 小时前
LangChain记忆:通过Memory记住上次的对话细节
人工智能·python·langchain
strayCat2325520 小时前
Clawdbot 源码解读 7: 扩展机制
人工智能·开源
王鑫星20 小时前
SWE-bench 首次突破 80%:Claude Opus 4.5 发布,Anthropic 的野心不止于写代码
人工智能
lnix20 小时前
当“大龙虾”养在本地:我们离“反SaaS”的AI未来还有多远?
人工智能·aigc