概述
上一篇做了测试策略,输入需求,输出JSON。但CI流水线不认JSON,它要的是pytest能跑的东西,这篇就把测试策略转成Pytest代码。
现在我们有策略,策略里啥都有,场景、预期、类型、优先级。把这些喂给AI,需求进去,代码出来,中间不需要人动手。
用例生成模块是做什么的
输入测试策略JSON,输出Pytest测试代码。
具体点说,输入是上一篇生成的策略,功能名称、场景列表、步骤、预期结果。输出是一个完整的测试文件,你拿过来就能用pytest命令跑。
为什么要拆成两步
把意图理解和用例生成合并成一步,输入需求,直接输出Pytest代码。听起来很爽,少了一道工序,但合在一起真的不好用。
可能带来的问题:
出错了你找不到原因,生成的代码挂了,你不知道是需求理解错了,还是代码生成环节出了岔子。日志里只有头和尾,中间发生了什么是个黑盒。
你想改没地方下手。策略生成错了,你想手动改一下再重新生成。如果合并成一步,你得重新跑一遍完整流程,API费用再花一次。
换个框架你得重写全部。同一个测试策略,你可能想生成pytest版本的,也可能想生成unittest版本的,合并的话每种框架你得维护一套完整逻辑。拆开的话策略模块不用动,只换生成模块就行。
所以拆开更实用
具体怎么做
3.1 核心实现
先写一个生成器类。
# test_generation.pyimportjsonimportosfrompathlibimportPathfromtypingimportDict,Optionalfromdotenvimportload_dotenvfromopenaiimportOpenAIclassTestGenerationAgent:""" 用例生成Agent。 输入测试策略JSON和源代码,输出Pytest测试代码。 """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=modeldefgenerate(self,strategy:Dict,source_code:str,module_name:str="app")->str:"""根据测试策略和源代码生成Pytest测试代码。"""prompt=self._build_prompt(strategy,source_code,module_name)try:response=self.client.chat.completions.create(model=self.model,messages=[{"role":"system","content":self._get_system_prompt()},{"role":"user","content":prompt}],temperature=0.3)returnresponse.choices[0].message.contentexceptExceptionase:print(f"用例生成失败:{e}")return""def_get_system_prompt(self)->str:return""" 你是一位资深的测试开发工程师,擅长用Pytest编写高质量的单元测试。 你的任务是根据提供的测试策略和源代码,生成完整的Pytest测试代码。 要求: 1. 测试代码必须覆盖策略中定义的所有场景 2. 使用pytest框架,遵循最佳实践 3. 每个测试场景对应一个测试方法 4. 方法名用test_开头,后面跟场景的英文标识 5. 使用pytest.raises处理异常场景 6. 添加适当的注释说明每个测试的用途 7. 代码风格清晰、规范 8. 只输出Python代码,不要额外解释 """def_build_prompt(self,strategy:Dict,source_code:str,module_name:str)->str:scenarios=strategy.get('test_scenarios',[])scenario_text=json.dumps(scenarios,indent=2,ensure_ascii=False)returnf"""#下面是待测试的源代码:{source_code}#下面是测试策略,包含需要覆盖的测试场景:{scenario_text}# 请为 {module_name} 模块生成完整的Pytest测试代码。defsave_test_code(code:str,filepath:str="test_generated.py"):withopen(filepath,'w',encoding='utf-8')asf:f.write(code)print(f"测试代码已保存:{filepath}")3.2 如何使用
加载上一篇生成的测试策略,传入源代码,调用生成器。
if__name__=="__main__":# 加载上一篇生成的测试策略withopen("login_strategy.json",'r',encoding='utf-8')asf:strategy=json.load(f)# 被测试的源代码source_code=""" # login.py class UserService: def __init__(self): self._users = { "13800001111": {"password": "123456", "nickname": "张三", "locked": False}, "test@example.com": {"password": "123456", "nickname": "李四", "locked": False}, } self._login_attempts = {} self._locked_accounts = {} def login(self, account: str, password: str) -> dict: if account not in self._users: return {"success": False, "message": "账号不存在"} user = self._users[account] if user.get("locked", False): return {"success": False, "message": "账号已被锁定,请联系客服"} if account in self._locked_accounts: lock_time = self._locked_accounts[account] if lock_time > 0: return {"success": False, "message": "账号已被锁定30分钟"} if user["password"] != password: self._login_attempts[account] = self._login_attempts.get(account, 0) + 1 if self._login_attempts[account] >= 5: self._locked_accounts[account] = 30 return {"success": False, "message": "因多次登录失败,账号已被锁定30分钟"} return {"success": False, "message": "密码错误"} self._login_attempts[account] = 0 return { "success": True, "message": "登录成功", "nickname": user["nickname"] } """agent=TestGenerationAgent()test_code=agent.generate(strategy,source_code,"login")iftest_code:save_test_code(test_code,"test_login.py")print("\n生成的测试代码:\n")print(test_code)else:print("生成失败")3.3 AI生成的代码
运行上面的脚本,test_login.py的内容大概是这样的。
# test_login.pyimportpytestfromloginimportUserServiceclassTestUserLogin:"""用户登录功能的测试套件"""defsetup_method(self):"""每个测试方法执行前重置状态"""self.service=UserService()deftest_login_with_phone_success(self):"""TC-001: 使用手机号正确登录"""result=self.service.login("13800001111","123456")assertresult["success"]isTrueassertresult["message"]=="登录成功"assertresult["nickname"]=="张三"deftest_login_with_email_success(self):"""TC-002: 使用邮箱正确登录"""result=self.service.login("test@example.com","123456")assertresult["success"]isTrueassertresult["message"]=="登录成功"assertresult["nickname"]=="李四"deftest_login_account_not_exist(self):"""TC-003: 登录失败-账号不存在"""result=self.service.login("13900001111","123456")assertresult["success"]isFalseassertresult["message"]=="账号不存在"deftest_login_wrong_password(self):"""TC-004: 登录失败-密码错误"""result=self.service.login("13800001111","wrongpass")assertresult["success"]isFalseassertresult["message"]=="密码错误"deftest_login_account_locked_by_admin(self):"""TC-005: 登录失败-账号被锁定"""self.service._users["13800001111"]["locked"]=Trueresult=self.service.login("13800001111","123456")assertresult["success"]isFalseassertresult["message"]=="账号已被锁定,请联系客服"deftest_login_trigger_lock_after_5_failures(self):"""TC-006: 连续输错密码触发锁定"""account="13800001111"foriinrange(5):result=self.service.login(account,"wrongpass")ifi<4:assertresult["message"]=="密码错误"else:assertresult["message"]=="因多次登录失败,账号已被锁定30分钟"assertresult["success"]isFalsedeftest_login_paste_disabled(self):"""TC-007: 密码框禁止粘贴"""# 这个场景是前端约束,单元测试测不了# 实际项目中需要UI自动化测试覆盖pytest.skip("前端约束,需要UI自动化测试验证")deftest_login_response_time(self):"""TC-008: 登录接口响应时间"""importtime start=time.time()self.service.login("13800001111","123456")elapsed=(time.time()-start)*1000assertelapsed<500,f"响应时间{elapsed}ms超过500ms"策略里的8个场景变成了8个测试方法。每个方法对应一个场景,断言也写好了。
3.3 把两步串起来
下面这个脚本把意图理解和用例生成串成一条线。
# end_to_end.pyimportjsonfromintent_understandingimportIntentUnderstandingAgentfromtest_generationimportTestGenerationAgentdefrun_end_to_end(requirement:str,source_code:str,module_name:str="app"):print("="*50)print("Step 1: 意图理解")print("="*50)intent_agent=IntentUnderstandingAgent()strategy=intent_agent.understand(requirement)if"error"instrategy:print(f"失败:{strategy['error']}")returnprint(f"生成{len(strategy.get('test_scenarios',[]))}个场景")withopen("strategy_output.json",'w',encoding='utf-8')asf:json.dump(strategy,f,indent=2,ensure_ascii=False)print("策略已保存: strategy_output.json")print("\n"+"="*50)print("Step 2: 用例生成")print("="*50)gen_agent=TestGenerationAgent()test_code=gen_agent.generate(strategy,source_code,module_name)ifnottest_code:print("生成失败")returnwithopen("test_output.py",'w',encoding='utf-8')asf:f.write(test_code)print("测试代码已保存: test_output.py")print("\n"+"="*50)print("完成!")print("="*50)print(f"场景数:{len(strategy.get('test_scenarios',[]))}")print("下一步: pytest test_output.py -v")if__name__=="__main__":requirement=""" 需求:用户登录功能 用户可以使用手机号或邮箱进行登录。 登录时需要验证密码是否正确。 登录成功后的行为: - 跳转到个人主页 - 在顶部显示用户昵称 登录失败的场景: - 手机号/邮箱不存在:提示"账号不存在" - 密码错误:提示"密码错误" - 账号被锁定:提示"账号已被锁定,请联系客服" 安全要求: - 连续输错5次密码后,账号锁定30分钟 """source_code=""" class UserService: def __init__(self): self._users = { "13800001111": {"password": "123456", "nickname": "张三", "locked": False}, } self._login_attempts = {} self._locked_accounts = {} def login(self, account: str, password: str) -> dict: if account not in self._users: return {"success": False, "message": "账号不存在"} user = self._users[account] if user.get("locked", False): return {"success": False, "message": "账号已被锁定,请联系客服"} if account in self._locked_accounts: return {"success": False, "message": "账号已被锁定30分钟"} if user["password"] != password: self._login_attempts[account] = self._login_attempts.get(account, 0) + 1 if self._login_attempts[account] >= 5: self._locked_accounts[account] = 30 return {"success": False, "message": "因多次登录失败,账号已被锁定30分钟"} return {"success": False, "message": "密码错误"} self._login_attempts[account] = 0 return {"success": True, "message": "登录成功", "nickname": user["nickname"]} """run_end_to_end(requirement,source_code,"login")跑完之后,执行:
pytest test_output.py-v看到测试通过的那一刻,整个流程就闭环了。需求进去,代码出来,中间不用你写一行。
常见问题
Q1:生成的代码能直接用吗?
最好检查一遍。
AI偶尔会犯错。import路径不对,或者依赖的类名写错了。大问题不多,小毛病难免。
但比自己从头写还是能省了至少一个小时的时间
Q2:代码跑不通怎么办?
把报错信息贴给AI,让它自己改。
这是目前最省事的做法。把pytest的输出复制下来,跟原始代码一起发给AI,告诉它"这段测试挂了,帮我修一下"。大部分情况下AI能自己搞定。
Q3:场景太多了生成慢怎么办?
拆成几批。
如果一个策略有20多个场景,你可以分两批生成。先让AI生成前10个,保存。再生成后10个,追加到同一个文件。
这样每次请求的数据量小一些,响应快,也不容易超出AI的上下文窗口。
Q4:项目代码风格不一样怎么办?
把规范写进prompt里。
比如你们项目要求测试类必须继承某个基类,或者必须用特定的fixture。把这些要求写进系统提示词,AI会照着做。
如果你想做得更细致,可以在项目里放一个规范文件,每次生成之前让AI先读一遍。
总结
意图理解管出策略,用例生成出代码,中间用JSON对接。
整个流程是这样的:
需求描述 → 测试策略 → Pytest代码 → pytest执行
前三步我们已经能自动跑通了,第四步就是pytest自己的事。