概述
前两篇我们搭好了环境,也看到了AI生成Pytest代码的效果,但AI是怎么知道要测什么的?
第一篇里,我们直接把除法函数的源代码喂给AI,让它生成测试用例。这种方式叫代码驱动。AI看实现来写测试。
但在真实项目里,你很少能只靠看代码就写出完整的测试。你需要读需求文档,需要跟产品经理对齐预期,需要知道业务规则是什么。
这就需要一个前置步骤。把人类的自然语言需求,转成结构化的测试策略。
这一篇,我们就来实现这个模块。从一段需求描述开始,让AI自动生成包含测试场景、边界条件、预期结果的完整测试策略。
意图理解模块是做什么的
用一句话说,输入自然语言,输出结构化的测试策略。
具体来看,输入是一段需求描述,比如产品经理发来的文档。输出是一个JSON格式的测试策略,包含这些字段:
{"feature":"功能名称","test_scenarios":[{"scenario_id":"TC-001","name":"场景名称","description":"场景描述","preconditions":["前置条件1","前置条件2"],"steps":["步骤1","步骤2","步骤3"],"expected":"预期结果","priority":"high/medium/low","type":"positive/negative/boundary"}]}这个JSON我们会直接喂给用例生成模块,也就是第一篇里那个生成Pytest代码的脚本,所以意图理解模块的输出格式,决定了后续所有环节的质量。
注意:意图理解模块只负责策略,它不生成任何Python代码,只输出结构化的测试设计
这样做有三个好处:
- 职责分离:意图理解管测什么,用例生成管怎么测。
- 可解释性:测试策略是JSON格式,人能直接读,也能直接改。
- 可复用:同一个测试策略,可以对接不同的代码生成逻辑。
为什么需要意图理解模块
我们先回顾一下第二篇的架构图,AI Agent测试系统有四个模块:
意图理解 → 用例生成 → 执行收集 → 分析修复
意图理解是第一环,传统模式下,这个环节是纯人工的,产品经理写一份需求文档,测试开发工程师阅读文档理解业务逻辑,设计测试场景,并把场景翻译成代码。
这道工序有三个问题:
- 信息损耗:需求文档写的和工程师理解的可能有偏差,尤其当需求写得模糊的时候,不同的人理解完全不同。
- 效率瓶颈:一个中等规模的需求,从阅读到提炼出完整测试策略,至少半天,需求频繁变更的话,这个工作要反复做。
- 知识流失:测试策略只存在于代码里,代码只告诉你测什么,不告诉你为什么这么测,导致老的测试工程师走了,新来的看不懂之前为什么设计这些用例。
AI Agent在这里的价值不是取代你理解需求,是帮你加速理解。
如何实现意图理解模块
理论讲完了,我们用代码来实际的理解一下
3.1 核心实现
# intent_understanding.pyimportjsonimportosfromtypingimportList,Dict,OptionalfrompathlibimportPathfromdotenvimportload_dotenvfromopenaiimportOpenAIclassIntentUnderstandingAgent:""" 意图理解Agent。 输入需求描述,输出结构化的测试策略JSON。 """def__init__(self,model:str="gpt-4o"):load_dotenv()self.client=OpenAI(api_key=os.getenv("OPENAI_API_KEY"),base_url=os.getenv("OPENAI_BASE_URL",None))self.model=modeldefunderstand(self,requirement:str)->Dict:"""输入需求描述,返回测试策略。"""prompt=self._build_prompt(requirement)try:response=self.client.chat.completions.create(model=self.model,messages=[{"role":"system","content":self._get_system_prompt()},{"role":"user","content":prompt}],temperature=0.2,response_format={"type":"json_object"})result=json.loads(response.choices[0].message.content)returnresultexceptExceptionase:print(f"意图理解失败:{e}")return{"error":str(e)}def_get_system_prompt(self)->str:return""" 你是一位资深的测试架构师,擅长从需求描述中提取测试策略。 你的任务是将用户提供的需求描述,转化为结构化的测试策略JSON。 输出格式必须严格遵循以下结构: { "feature": "功能名称", "description": "功能描述", "test_scenarios": [ { "scenario_id": "TC-001", "name": "场景名称", "description": "场景描述", "preconditions": ["前置条件1", "前置条件2"], "steps": ["操作步骤1", "操作步骤2", "操作步骤3"], "expected": "预期结果", "priority": "high|medium|low", "type": "positive|negative|boundary" } ], "total_scenarios": 数字, "coverage_notes": "覆盖情况说明" } 设计原则: 1. 每个需求至少包含正常路径和异常路径 2. 边界条件是加分项 3. 每个场景的expected必须可验证 4. 优先级:核心功能用high,异常场景用medium,边界场景用low 5. 场景之间不要重复覆盖 """def_build_prompt(self,requirement:str)->str:returnf""" 请根据以下需求描述生成测试策略: ---{requirement}--- 请输出JSON格式的测试策略。 """defsave_strategy(strategy:Dict,filepath:str="test_strategy.json"):withopen(filepath,'w',encoding='utf-8')asf:json.dump(strategy,f,indent=2,ensure_ascii=False)print(f"测试策略已保存:{filepath}")if__name__=="__main__":requirement=""" 需求:用户登录功能 用户可以使用手机号或邮箱进行登录。 登录时需要验证密码是否正确。 登录成功后的行为: - 跳转到个人主页 - 在顶部显示用户昵称 - 在右上角显示用户头像 登录失败的场景: - 手机号/邮箱不存在:提示"账号不存在" - 密码错误:提示"密码错误" - 账号被锁定:提示"账号已被锁定,请联系客服" 安全要求: - 连续输错5次密码后,账号锁定30分钟 - 密码输入框不支持粘贴 性能要求: - 登录接口响应时间小于500ms(P95) """agent=IntentUnderstandingAgent()strategy=agent.understand(requirement)if"error"notinstrategy:save_strategy(strategy,"login_strategy.json")print(f"\n测试策略摘要:")print(f" 功能:{strategy.get('feature','未知')}")print(f" 场景数:{strategy.get('total_scenarios',0)}")forscinstrategy.get('test_scenarios',[]):print(f" - [{sc['type']}]{sc['name']}(优先级:{sc['priority']})")else:print(f"生成失败:{strategy['error']}")3.2 输出示例
运行上面的脚本,生成的login_strategy.json:
{"feature":"用户登录","description":"用户通过手机号或邮箱进行身份验证,成功进入个人主页","test_scenarios":[{"scenario_id":"TC-001","name":"使用手机号正确登录","description":"用户使用正确的手机号和密码登录","preconditions":["用户已注册,手机号13800001111,密码正确"],"steps":["打开登录页面","输入手机号 13800001111","输入正确密码","点击登录按钮"],"expected":"跳转到个人主页,顶部显示用户昵称,右上角显示用户头像","priority":"high","type":"positive"},{"scenario_id":"TC-002","name":"使用邮箱正确登录","description":"用户使用正确的邮箱和密码登录","preconditions":["用户已注册,邮箱 test@example.com,密码正确"],"steps":["打开登录页面","输入邮箱 test@example.com","输入正确密码","点击登录按钮"],"expected":"跳转到个人主页","priority":"high","type":"positive"},{"scenario_id":"TC-003","name":"登录失败-账号不存在","description":"使用不存在的账号登录","preconditions":[],"steps":["打开登录页面","输入手机号 13900001111(未注册)","输入任意密码","点击登录按钮"],"expected":"提示'账号不存在',停留在登录页","priority":"high","type":"negative"},{"scenario_id":"TC-004","name":"登录失败-密码错误","description":"密码输入错误","preconditions":["用户已注册,手机号13800001111"],"steps":["打开登录页面","输入手机号 13800001111","输入错误密码","点击登录按钮"],"expected":"提示'密码错误',停留在登录页","priority":"high","type":"negative"},{"scenario_id":"TC-005","name":"登录失败-账号被锁定","description":"已锁定账号无法登录","preconditions":["账号已被管理员锁定"],"steps":["打开登录页面","输入锁定账号的手机号","输入正确密码","点击登录按钮"],"expected":"提示'账号已被锁定,请联系客服',停留在登录页","priority":"medium","type":"negative"},{"scenario_id":"TC-006","name":"连续输错密码触发锁定","description":"连续5次密码错误后账号锁定30分钟","preconditions":["用户已注册,手机号13800001111"],"steps":["打开登录页面","输入手机号 13800001111","输入错误密码","点击登录按钮","重复5次"],"expected":"第5次错误后提示'账号已被锁定30分钟',30分钟内无法登录","priority":"high","type":"boundary"},{"scenario_id":"TC-007","name":"密码框禁止粘贴","description":"密码输入框不允许粘贴操作","preconditions":[],"steps":["打开登录页面","在密码框尝试粘贴"],"expected":"粘贴无效,密码框内容不变","priority":"low","type":"negative"},{"scenario_id":"TC-008","name":"登录接口响应时间","description":"验证登录接口响应时间","preconditions":["系统正常运行"],"steps":["发送登录请求","记录响应时间"],"expected":"响应时间小于500ms","priority":"medium","type":"positive"}],"total_scenarios":8,"coverage_notes":"覆盖手机号登录、邮箱登录、账号不存在、密码错误、账号锁定、连续错误锁定、禁止粘贴、响应时间,包含2个正向、4个负向、2个边界场景"}8个测试场景,涵盖正向、负向、边界,功能、安全、性能三个维度,如果人工来做的话,半天工作量。但是交给AI则10秒。
3.3 加上输出校验
AI的输出有时候不稳定。字段缺失、类型错误的情况偶尔会出现,生产环境里,不要直接把AI的输出喂给下游。先做校验。
用Pydantic定义一个严格的输出模型:
# intent_understanding_with_validation.pyfrompydanticimportBaseModel,Field,validatorfromtypingimportList,OptionalimportjsonclassTestScenario(BaseModel):scenario_id:str=Field(...,description="场景编号")name:str=Field(...,description="场景名称")description:str=Field(...,description="场景描述")preconditions:List[str]=Field(default_factory=list)steps:List[str]=Field(...,description="操作步骤")expected:str=Field(...,description="预期结果")priority:str=Field(...,description="high/medium/low")type:str=Field(...,description="positive/negative/boundary")@validator('priority')defvalidate_priority(cls,v):allowed=['high','medium','low']ifv.lower()notinallowed:raiseValueError(f'priority必须是{allowed}之一')returnv.lower()@validator('type')defvalidate_type(cls,v):allowed=['positive','negative','boundary']ifv.lower()notinallowed:raiseValueError(f'type必须是{allowed}之一')returnv.lower()classTestStrategy(BaseModel):feature:str=Field(...,description="功能名称")description:str=Field(...,description="功能描述")test_scenarios:List[TestScenario]total_scenarios:intcoverage_notes:Optional[str]=None@validator('total_scenarios')defvalidate_total(cls,v,values):if'test_scenarios'invalues:actual=len(values['test_scenarios'])ifv!=actual:raiseValueError(f'total_scenarios应该是{actual},实际是{v}')returnvdefvalidate_strategy(raw:Dict)->Optional[TestStrategy]:try:returnTestStrategy(**raw)exceptExceptionase:print(f"校验失败:{e}")returnNone有了这个校验,下游收到的数据一定是合法的。
3.4 导出Markdown报告
JSON是给机器用的。团队里其他人要看测试策略,给一份可读性好的文档。
defstrategy_to_markdown(strategy:Dict,output_file:str="test_strategy.md"):lines=[]lines.append(f"# 测试策略:{strategy.get('feature','未命名')}\n")lines.append(f">{strategy.get('description','')}\n")lines.append(f"**场景总数**:{strategy.get('total_scenarios',0)}\n")type_count={'positive':0,'negative':0,'boundary':0}forscinstrategy.get('test_scenarios',[]):t=sc.get('type','')iftintype_count:type_count[t]+=1lines.append("**场景分布**:")lines.append(f"- 正向:{type_count['positive']}个")lines.append(f"- 负向:{type_count['negative']}个")lines.append(f"- 边界:{type_count['boundary']}个\n")lines.append("---\n")forscinstrategy.get('test_scenarios',[]):priority_label={'high':'高','medium':'中','low':'低'}type_label={'positive':'正向','negative':'负向','boundary':'边界'}lines.append(f"##{sc.get('scenario_id')}:{sc.get('name')}")lines.append(f"- **类型**:{type_label.get(sc.get('type',''),sc.get('type'))}")lines.append(f"- **优先级**:{priority_label.get(sc.get('priority','low'),'低')}")lines.append(f"- **描述**:{sc.get('description','')}")ifsc.get('preconditions'):lines.append("- **前置条件**:")forpinsc.get('preconditions',[]):lines.append(f" -{p}")ifsc.get('steps'):lines.append("- **操作步骤**:")foridx,stepinenumerate(sc.get('steps',[]),1):lines.append(f"{idx}.{step}")lines.append(f"- **预期结果**:{sc.get('expected','')}\n")lines.append("---\n")withopen(output_file,'w',encoding='utf-8')asf:f.write("\n".join(lines))print(f"Markdown报告已生成:{output_file}")现在一份需求进去,出来三样东西:
- JSON文件,给下游模块用
- Pydantic对象,在代码里用,有类型安全
- Markdown报告,给人看,发给团队review
常见问题
Q1:为什么输出JSON,不直接输出代码?
JSON和代码的可读性不一样,如果AI生成的代码有bug,你得先看着代码改,生成的策略漏了场景,你还是得在代码里改,写过代码的同学都体会过改别人代码的痛苦。
JSON不一样,你打开文件,加一个场景,填几个字段,改完再让AI重新生成代码就行。
Q2:AI生成的测试策略能直接用吗?
不能。你得看一遍。
有时候AI会漏场景,有时候对业务规则理解偏了。但你review一个JSON比从零写快很多。
核心原则就一条:AI出初稿,你做判断。
总结
这篇做了个东西:意图理解模块。输入是一段需求描述,输出是一份测试策略JSON。
包含了三层东西:
- 核心调用:把需求发给LLM,返回结构化的JSON。
- 输出校验。用Pydantic卡一道,字段不对就报错,确保下游拿到的数据是干净的。
- Markdown报告,转成文档发给团队,能看能改。
确定了一个设计:
输出JSON,不是直接出代码。因为可改、可复用、可审查。
现在第一环搭好了,之后我们就可以把意图理解和用例生成连起来,需求进去,测试代码出来。