一、前言1.什么是自动化自动化是通过技术手段把人力从简单繁琐的内容解放出来的一种技术。当前自动化大致分为两种类型接口自动化、UI自动化其中UI自动化又可以细分出三种app自动化、web自动化、小程序自动化。而接口自动化的实现方式也是多种多样目前较为主流的有python、jmeter、robotframework甚至有用apifox和postman的。可见如何实现自动化并不重要怎么用好自动化提升效率才是我们的最终目的。2.自动化的优劣优势快速性短时间内执行大量用例、可重复性用例可以反复执行、准确性代码比人更精准1就是12就是2、降低人力成本……劣势前期准备时间长、维护成本高、不能像人一样灵活的处理各种情况比如断言返回值是1但实际返回的值是字符串的1断言会报错……3.做自动化前的准备在做自动化之前我们要思考清楚几个问题我们要使用自动化来解决什么问题是为了解放人力解决繁琐的回归验证还是为了验证底表数据是否正确又或者只是为了用来mock数据以方便测试。只有先搞清楚这个问题才能开展接下来的工作否则可能造成南辕北辙的效果。我们要怎么开展自动化针对我们要解决的问题我们要使用什么方式来处理是比较好的是使用代码的形式编写脚本还是用jmeter这种功能已经较为完善的图形化工具还是使用postman这类api工具来快速地搭建一套简单的冒烟测试框架。这个需要具体问题具体分析不仅需要考虑解决问题的技术层面还要考虑易用性、拓展性、项目人员技术栈等内容。二、选择那么以上讲了这么多我们该怎么去选择自己需要的技术和解决方案呢还是回到一开始说的问题我们是为了处理什么问题才开展自动化。1、目前大家开展自动化较为主流的目的都是为了解决转测冒烟和上线前后的回归验证问题。针对这个目的我们还得具体展开分析例如你在开展接口自动化的过程中是否需要解决参数加解密问题如果需要的话哪种工具能够更快更好的实现这个功能。又或者在自动化流程中是否要依赖到数据库中的一些数据一般只要涉及到跟数据库又或是要涉及从前端页面中抓取某些数据的话我是推荐写代码来开展工作的。Python的pytest、unittest框架pymysql、beautifulSoup等第三方库都可以很好的糅合在一起自由的实现你需要的功能。如果并不涉及跟数据库的交互仅用jmeter等工具也能实现那么用工具也是一个好的选择。还有一种比较特殊的情况针对某一个功能需要做冒烟测试仅校验接口是不是能ping通有无报错返回值是否正确那么用postman也不错能够很快的实现。2、有的时候做自动化并不是为了校验接口而是为了校验底表数据是否正确。举个例子在ERP系统中整条数据流是非常长且复杂的。你从接口取一个销售额的字段可能经过四五张表以及好几个任务的计算那么这种情况下底表的数据可能是更重要的一旦底表数据出错那么后续修复起来会非常麻烦甚至因为难以追溯导致一些数据无法修复。那么这种情况下你仅做接口的校验是不太保险的就需要加上底表的数据验证。由于解决这种问题涉及大量的数据库交互以及任务调度因此通过python或Java等代码来实现可能会更好一些。三、思路假如现在要用python写一套接口自动化框架该怎么做1、首先还是刚才一直强调的一件事先搞清楚到底这套框架是用来解决什么问题的用什么方法来解决会比较好用2、针对这套框架我们需要用到的功能有哪些以后有没有可能往其他方面拓展是只有自己用还是多人协作3、设计框架架构先确定整个数据流大致是怎么走的。从main文件开始中间需要经过哪些组件处理4、确定框架架构后进行组件功能的实现等组件写完后再进行拼接联调让整条数据链路打通。5、整个框架联调完成通过测试后就可以开始写用例了到这时便需要一定的时间进行用例的积累才能发挥出自动化真正的价值。6、等用例积累到一定程度可以考虑集成流水线开放入口给项目组成员并通过webhook的方式推送测试报告。PS在框架搭建、用例编写的过程中肯定会遇到很多莫名其妙的问题要学会面向百度编程四、样例针对于Willdesk的项目我编写了一套接口自动化的框架。接下来会尽量地从零到一告诉大家一套接口自动化框架怎么搭建前期准备首先在做这套框架之前我大致分析了一下我需要解决的问题这套框架主要是用于冒烟测试和上线前后的回归验证这是个长期的项目而且后期可能不止我一个人编写因此需要考虑团队协作以及拓展性和易用性要求的问题。要做到让没有太多代码基础的人也能够写自动化用例有的关键参数不能直接从接口获取需要通过爬虫或加解密的方式处理涉及websocket协议是否还有其他的难点目前还未发现一些数据需要通过跟数据库交互才能获取/预先插入/后置删除后期打算集成到流水线上让开发/产品也能使用针对以上几个问题最终选用的技术是pytestrequestswebsocket框架介绍前言pytest是一个非常成熟的全功能的Python测试框架。简单灵活容易上手支持参数化测试用例的skip和xfail自动失败重试等处理能够支持简单的单元测试和复杂的功能测试还可以用来做selenium/appnium等自动化测试接口自动化测试pytestrequestspytest具有很多第三方插件并且可以自定义扩展比较好用的如pytest-allure(完美html测试报告生成)pytest-xdist(多CPU并发)等可以很好的和jenkins集成在编写代码前我们要先安装一下pytest的第三方包打开cmd命令行pip install pytest #安装pytest第三方包 pytest --version #是否安装成功输入pytest --version 后出现版本号即为安装成功规则pytest框架对于case文件有规则要求a) 文件test_.py开头和_test.py结尾b) Test开头的类中test开头的方法测试类不能带有__init__方法c) 模块中test开头的函数可以不在class中参考该图注意点1、pytest是以方法为单位发现用例的你写不写测试类根本不重要在哪个目录下执行pytest就在哪个目录下按照上述规则去查找所以我这么写也可以2、如果对文件的执行顺序有要求可以参考图中的方式进行命名pytest会从01开始往后执行越靠近01越先执行。而文件内部的函数则根据位置先后决定执行顺序即写得越靠前的函数越先执行参数以下是运行pytest命令时的一些参数打印详细运行日志信息pytest -v (最高级别信息-verbose)s是带控制台输出结果也是输出详细可以打印测试用例中print的输出pytest -v -s 文件名执行单独一个pytest模块pytest 文件名.py运行某个模块里面某个类pytest 文件名.py::类名运行某个模块里面某个类里面的方法pytest 文件名.py::类名::方法名跳过某个用例pytest -k 类名 and not 方法名如pytest -k TestDemo and not test_one运行带有某标记的测试用例(pytest.mark.标记名)pytest -m 标记名一旦运行到报错就停止运行pytest -x 文件名当错误达到num的时候就停止运行pytest --maxfail[num]代码编写main框架需要一个入口来控制用例的执行这个入口就是main文件pytest.main是pytest第三方包自带的函数可以理解为他帮你执行cmd命令。只需要传入参数即可。参数用数组的形式传入数组中用不同的变量隔开 如以下转换成cmd命令为pytest -vs ./TestCase/FAQ/test_01_article.py --html./report.html这段命令的意思是 执行TestCase/FAQ目录下名为test_01_article.py的文件执行时要输出详细的结果执行后在当前目录生成名为report.html的测试报告。而finally:test_env_conn.close_db()这段代码意思是无论执行结果如何都关闭数据库链接。如果不及时关闭当连接数量过多可能导致数据库连接池打满影响服务正常运行import pytest #引入第三方包 from Utils.sql_handler import test_env_conn #引入sql组件 if __name__ __main__: try: pytest.main([-vs,./TestCase/FAQ/test_01_article.py,f--html./report.html]) #allure报告一直生成不了改用pytest自带报告 finally: test_env_conn.close_db() #关闭数据库链接conftest介绍conftest.py是pytest特有的本地测试配置⽂件既可以⽤来设置项⽬级的Fixture,也可以⽤来导⼊外部插件还可以⽤来指定Hook函数钩⼦函数conftest.py配置需要注意- conftest文件名是不能自定义的不能更改- conftest.py与运行的用例要在同一个package下并且有__init__.py文件- 不需要import导入conftest.py pytest用例会自动查找用法我们可以在conftest文件中处理一些前后置步骤例如在正式开始测试前我们需要先链接数据库、先处理登录态并把登录态保存起来、先把websocket服务起了。又或者测试结束后我要把服务关掉。类似这些操作都可以放在conftest文件中处理。如下图图中的代码是什么意思呢设置了pytest.fixture装饰器后pytest在运行时会自动找到这个函数并执行装饰器中的scope代表了执行域当设置为session时则整个测试过程中只会运行一次PS装饰器的概念可以这么理解想象你在一家餐厅工作每道菜都需要经过一系列的烹饪步骤才能完成。现在你需要在每道菜做完后进行装饰例如添加装饰用的花朵或香料。这里的烹饪步骤就相当于原始函数而添加花朵或香料的过程就相当于装饰器。即不改变原有函数的功能只在功能执行前后的过程做一些其他的动作。比如计算每个函数执行的时间便可以通过装饰器来完成。函数中则是运行过程中会执行的代码。要注意函数中的yield关键字。当加上了yield关键字后在执行测试用例case文件前会先执行yield关键字前的代码即图中的willdesk_login等内容。而在所有的case文件执行结束后会运行yield关键字后的代码即loop.run_until_complete等内容注意要使用该装饰器要先导入pytest第三方包最后在上图的代码执行后还会执行接下来这段代码。这段代码的具体作用是收集测试结果并通过webhook推送到群里。这段代码不需要我们调用pytest框架在所有的操作执行结束后会自己去执行的。至于函数名称以及一些参数名称不建议进行修改这段代码也是我百度copy的修改后能不能执行我也不知道不过大家可以自己试试def pytest_terminal_summary(terminalreporter, exitstatus, config): 收集测试结果 print(total:, terminalreporter._numcollected) print(passed:, len(terminalreporter.stats.get(passed, []))) print(failed:, len(terminalreporter.stats.get(failed, []))) print(error:, len(terminalreporter.stats.get(error, []))) print(skipped:, len(terminalreporter.stats.get(skipped, []))) # terminalreporter._sessionstarttime 会话开始时间 duration time.time() - terminalreporter._sessionstarttime print(total times:, round(duration,2), seconds) webhook_handler.webhook.sendAutotestReport(resultterminalreporter,secondround(duration,2))推送效果图具体的钉钉webhook使用文档参考该链接https://open.dingtalk.com/document/orgapp/custom-robots-send-group-messagesTestCase那么在了解了以上内容后我们可以开始框架的初步搭建。首先我们知道不同用例是通过不同的case文件区分的针对这一点我们可以给用例文件专门设置一个文件夹。如图在TestCase文件夹中我给不同模块的用例区分了不同的文件夹这样目录较为清晰便于管理且后面我们想单独测试某个模块的时候也可以通过参数传入对应的目录地址达到目的YamlCaseTestCase中的py文件仅是作为一个执行入口与配置对应的变量类、yaml用例使用YamlCase中的文件才是用例具体执行的内容。在Yamlcase目录下会存放对用的yaml用例文件目录结构与TestCase一样具体的yaml文件格式可以参考以下链接https://blog.csdn.net/qq_23994929/article/details/133099851在写用例时只需要填入url、method、expected等值即可以上关键字都会在req_handler组件中进行识别处理TestData接下来回到变量的问题在框架中每一个用例文件都对应着一个变量类变量类则存放在TestData文件夹中。变量类继承Global公用类后可以访问一些公用属性例如token、shopid……也可以预先设置私有属性如下图。此外还可以与TestFile进行配合在执行用例前通过sql获得变量值然后赋值到私有类中。TestData的目录结构与TestCase一致TestFileTestFile是用来存放测试所需文件的地方例如用例对应的sql文件也可以存放其他的文件具体使用根据情况而定。TestFile的目录结构与TestCase一致Config用来存放配置文件的文件夹类似账号密码、不同环境的url等内容可以放在这里面与config_handler组件配合使用Utils各种组件都存放在Utils的文件夹中例如处理数据库交互的组件、处理日志的组件。接下来会讲一些比较重要不可缺少的内容至于log等不必要的内容则先不介绍path_handlerpath_handler是用来专门处理路径的一个组件在搭建框架时不建议直接写死绝对路径如果直接写死绝对路径代码换一台电脑运行可能就会报错最好通过os库来处理文件路径。如下方代码Base_dir变量会取出项目根目录的绝对路径其他的变量则是基于Base_dir进行拼接。取出来的路径可以参考注释。其他文件需要使用时直接引用取变量即可import os Base_dir os.path.dirname(os.path.dirname(os.path.abspath(__file__))) #C:\\Users\\rd71\\PycharmProjects\\willdesk_api_auto Utils_dir os.path.join(Base_dir,Utils) #C:\\Users\\rd71\\PycharmProjects\\willdesk_api_auto\\Utils TestCase_dir os.path.join(Base_dir,TestCase) Config_dir os.path.join(Base_dir,Config) TestFile_dir os.path.join(Base_dir,TestFile) TestData_dir os.path.join(Base_dir,TestData) YamlCase_dir os.path.join(Base_dir,YamlCase)yaml_handler从前面可以知道虽然TestCase的用例py文件可以直接关联到对应的yaml文件但是想要让yaml文件能够变成我们想要各数据格式还是得经过处理的yaml_handler就是专门用来处理yaml文件的一个组件。下方代码中的get_case函数 会接收一个file_path的变量这个file_path对应Testcase中事先定义的yaml_path路径。在接收到路径后会进行路径的拼接获取yaml用例文件的绝对路径。然后再读取文件内容返回一个数组套字典的对象。如果传入的路径是错误的则会抛出异常停止当前用例的运行import yaml import os from Utils import path_handler class yamlHandler: def get_case(self,file_path): try: while file_path[0] / or file_path[0] \\: file_path file_path[1:] file_path os.path.join(path_handler.YamlCase_dir,file_path) with open(file_path,rb) as f: data yaml.load(f,yaml.Loader) return [data[case] for case in data] #以数组套字典的格式返回 except FileNotFoundError: print(fyaml用例文件未找到传入路径{file_path}) raise Exception yaml_handler yamlHandler()global_variate前面说到变量时经常说到公用类、私有类global_variate就是公用类的文件这个文件其实很简单只有两行代码定义了一个类里面什么都不写。他唯一的作用就是作数据存储不需要作任何其他的事情。class Global: 公用变量类所有的子变量类继承该类存取公用变量公用变量类默认存token等基础信息 sql_handler在接口自动化测试时经常需要跟数据库打交道比如有些时候你需要的字段接口是没有返回给你的你需要自己先从数据库里查出来才行这时候sql_handler就派上用场了。在sql_handler中引用了pymysql的第三方库。这是一个专门用来处理mysql的第三方库。需要自己安装命令pip install pymysql下方代码中先是在init函数定义了数据库地址等内容然后生成了一个数据库链接。注意conn变量中的cursorclass参数当设置cursorclasspymysql.cursors.DictCursor时代表你取到的数据会以字典的格式返回给你这可以让你省去处理数据格式的步骤很方便。在跟数据库交互时一般也不会有太复杂的内容普通的增删改查语句比较多因此只需要关注增删改查满足使用就可以了。当遇到更复杂的场景时可以在TestFile的sql文件中处理。import pymysql from Utils.config_handler import base_config class SqlHandler: def __init__(self,address,port,password,account,database willdesk): self.address address self.port port self.password password self.account account self.database database self.conn pymysql.connect(hostaddress,portport,useraccount,passwordpassword,databasedatabase,cursorclasspymysql.cursors.DictCursor) self.cursor self.conn.cursor() def select_one_value(self,sql): self.cursor.execute(sql) result self.cursor.fetchone() return result def select_many_value(self, sql): self.cursor.execute(sql) result self.cursor.fetchall() return result def execute_sql(self,sql): try: self.cursor.execute(sql) self.conn.commit() except Exception: self.conn.rollback() def close_db(self): print(已关闭) self.cursor.close() self.conn.close() test_env_conn SqlHandler(addressbase_config.get_value(mysql,address),portint(base_config.get_value(mysql,port)),accountbase_config.get_value(mysql,account),passwordbase_config.get_value(mysql,password))req_handlerreq_handler是整套框架的核心可以理解为整个流程的调度器。看下方的这个函数这个函数会在TestCase中执行class TestAccount: yaml_path raccount/account.yaml yaml_data yaml_handler.yaml_handler.get_case(yaml_path) pytest.mark.parametrize(case,yaml_data) def test_account(self,case): req_handler.ReqHandler.send_requests(casecase,var_classaccountData) print(case)它会接收两个变量case是处理好的yaml用例var_class则是对应用例的变量类。执行时会对yaml中的参数进行处理例如有无前后置sql文件要执行如果有的话根据路径找到sql文件然后通过exec函数执行内容。exec函数的作用可以理解为是将传入的字符串当作python代码运行。在对yaml文件中的参数处理后则会通过requests第三方库发送请求在发送请求前会判断请求头因为post方法的请求根据content-type的不同是要使用不同的参数的。发送请求后则会把返回体直接传给assert_handler专门断言的组件进行断言处理如果断言成功才会继续走下面的逻辑如果失败则下面的代码也没有必要执行了。在断言过后会判断是否需要设置变量以及是否需要睡眠等待之所以写一步睡眠等待是因为在实际进行测试的过程中发现有些接口执行完后可能需要过一两秒结果才会入库如果在没入库之前立马执行下一个用例可能会导致用例报错def send_requests(self,case,var_class): try: if isinstance(case,dict): title case[title] if case.get(before_sql): sql case[before_sql] while sql[0] \\ or sql[0] /: sql sql[1:] with open(os.path.join(path_handler.TestFile_dir,sql).replace(\\,/),moder,encodingutf8) as f: content f.read() exec(content) case self.params_handler(self,casecase,var_classvar_class) if case.get(ws): #识别到ws字段后直接走ws_request函数不继续往下面走 self.ws_requests(self,case) if case.get(after_sql): sql case[after_sql] while sql[0] \\ or sql[0] /: sql sql[1:] with open(os.path.join(path_handler.TestFile_dir, sql).replace(\\, /), moder, encodingutf8) as f: content f.read() exec(content) if case.get(sleep): time.sleep(float(case[sleep])) return 1 url case[url] if http in case[url] else config_handler.base_config.get_value(url,test_address) case[url] #判断是否有域名没有的话给config文件中的默认测试域名 method case[method] expected case[expected] if case.get(data): data case[data] else: data {} headers {Authorization : getattr(global_variate.Global,access_token),User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36} #如果没有请求头默认赋予一个带token的head if case.get(headers): headers.update(case[headers]) if method post: if headers.get(content-type) application/x-www-form-urlencoded or headers.get(content-type) text/xml: res requests.request(urlurl,methodmethod,datadata,headersheaders) elif headers.get(content-type) multipart/formdata: res requests.request(urlurl,methodmethod,filesdata,headersheaders) else: res requests.request(urlurl,methodmethod,jsondata,headersheaders) else: res requests.request(urlurl,methodmethod,datadata,headersheaders) self.assert_handler(self,resres,expectedexpected) if case.get(set_value): self.set_value_handler(self,resres,itemcase[set_value],var_classvar_class) if case.get(after_sql): sql case[after_sql] while sql[0] \\ or sql[0] /: sql sql[1:] with open(os.path.join(path_handler.TestFile_dir, sql).replace(\\, /), moder, encodingutf8) as f: content f.read() exec(content) if case.get(sleep): time.sleep(float(case[sleep])) logger.info(f{title}用例执行成功) else: raise TypeError(用例格式有误) except AssertionError: logger.error(f{title}用例执行失败失败原因断言不通过\n期望值{expected}\n实际返回值{res.text}) raise AssertionError except Exception as e: logger.error(f{title}用例执行失败失败原因{e}) raise Exceptionassert_handlerassert_handler是专门用来作断言处理的组件。组件会接收两个变量一个是返回体一个是期待结果在进行断言时会循环所有的期待结果期待结果中会以{jsonpath : value}的形式解析。key代表断言值在返回体中的jsonpath路径value代表期望值。当然有时候想要对比的值不一定在返回体中可能是一个固定的值所以res_path_value变量做了一次判断如果key不是以$.开头则按普通字符串处理。在该组件中定义了几种断言方式eq完全相等like值相等但是类型可能不相等not_eq不相等almost几乎相等用于带有小数点的数字对比会四舍五入成两位小数进行对比in断言值在返回值中ain返回值在断言值中not_in断言值不在返回值中a_not_in返回值不在断言值中exec最特殊的一种执行python代码因为有时候以上的断言方式无法满足需要因此需要通过python代码进行判断处理jsonpath使用文档https://blog.csdn.net/fanfangyu/article/details/133988514def assert_handler(self,res,expected): 处理断言的函数 :param res: 传入返回体 :param expected: 传入期望结果 :return: try: assert res.status_code 200 #先断言状态码是正确的 if isinstance(expected,list): 穿进来的expected一定是 {eq:{$.code : value}} 的形式要断言的值一定是通过jsonpath去检索的 for item in expected: #循环每一个断言场景 for item_key,item_value in item.items(): assert_way item_key path list(item_value.keys())[0] #拿到path路径 value list(item_value.values())[0] res_path_value jsonpath.jsonpath(res.json(),path)[0] if str(path)[0:2] $. else path if isinstance(res_path_value,str) and {contentList: in res_path_value: res_path_value res_path_value.replace(\\,) #兼容websocket当遇到包含{contentList:内容的值且类型是字符串时会自动去除所有\号 if assert_way eq: #eq代表完全相同 assert value res_path_value elif assert_way like: #代表值相同但是type可能不同 assert str(value) str(res_path_value) elif assert_way not_eq: #not_eq代表不相同 assert value ! res_path_value elif assert_way almost: #almonst代表四舍五入后相同就判断为一致 assert round(value,2) round(res_path_value,2) elif assert_way in: #in代表断言值在返回值中 assert value in res_path_value elif assert_way ain:#ain代表返回值在断言值中 assert res_path_value in value elif assert_way not_in: assert value not in res_path_value elif assert_way a_not_in: assert res_path_value not in value elif assert_way exec: exec(value) except AssertionError: print(f期望值为{value}{type(value)}实际返回值为{res_path_value}{type(res_path_value)}完整的expected体为{expected}) raise AssertionErrorparams_handlerparams_handler是用来处理变量的一个组件之前光说了变量是怎么存储的却没有说变量怎么使用。关于变量的使用方法可以看以下的代码。params_handler会接收两个变量一个是用例一个是变量类。接收到用例后会先把字典格式的用例转为字符串然后通过re正则表达式识别出case里面所有格式为${变量名}的内容。如果没有识别到则直接结束如果识别到了。会循环取出${}中的变量名然后拿这个变量名去传入的变量类中查找是否有对应变量名的变量。如果找到了则取出变量值替换掉case中的${变量名}。如果case中写了${变量名}但是拿着变量名去变量类中查找的时候却找不到则抛出异常报错def params_handler(self,case,var_class): 用来处理替换${}变量的方法 :param case: 传入需要替换变量的用例字典 :param var_class: 传入对应的变量类变量类必须先继承Global公用变量类 :return: 返回处理好的case字典 case str(case) replace_words re.findall(r\$\{(.?)\},case) if not replace_words: return eval(case) else: for word in replace_words: try: value getattr(var_class,word) #找到正确的变量值 if not isinstance(value,str): case case.replace(r${%s}%word,str(value)) #替换掉${}变量符 else: case case.replace(r${%s}%word,str(value)) #替换掉${}变量符 if re.findall(r\$\{(.?)\},case): #保底 case case.replace(r${%s}%word,str(value)) except Exception: import traceback print(traceback.print_exc()) print(f{word}变量未找到) raise Exception return eval(case)set_value_handlerset_value_handler是专门用来设置变量的一个组件在做自动化的时候经常会遇到一种情况A接口的某个变量是B接口所需要的。这个时候set_value_handler就能派上用场它会接收三个变量一个是返回体一个是要设置的值还有一个是变量类。要设置的值以{key名 jsonpath路径key名2 jsonpath路径} 的格式传入会取jsonpath路径去返回体中查找查到了就取key名为变量名设置到传入的变量类中没有的话就报错def set_value_handler(self,res,item,var_class): :param res: 传response请求体 :param item: 以{key名 jsonpath路径key名2 jsonpath路径} 的格式传入要设置的值 :return: for k,v in item.items(): value jsonpath.jsonpath(res.json(),v) if value: setattr(var_class, k, value[0]) else: raise Exception(f没有找到{k}值对应jsonpath{v})五、demo讲了这么多拿一个demo来演示下怎么写一条用例首先新建一个用例py文件用例文件中yaml_path写yaml文件的相对路径然后调用yaml_handler组件处理yaml文件。再定义一个test开头的函数去调用send_requests组件进行请求。注意这个函数要带pytest.mark.parametrize装饰器参数传case和处理好的yaml_data。import pytest from Utils import yaml_handler from Utils import req_handler from TestData.customer.customer_test_data import customerData class TestCustomer: yaml_path rcustomer\customer.yaml yaml_data yaml_handler.yaml_handler.get_case(yaml_path) pytest.mark.parametrize(case,yaml_data) def test_customer(self,case): req_handler.ReqHandler.send_requests(casecase,var_classcustomerData) print(case)定义用例py文件后新建一个变量类。变量类要继承公用类如果有需要提前定义的变量可以写在这里面from Utils.global_variate import Global class customerData(Global): customer_test_name autotest_customer customer_test_email autotest_customergmail.com customer_test_phone 13611122222 customer_test_note autotest_note_msg接着如果有要执行的sql文件新建一个sql处理文件。文件引用提前定义好的sql处理组件然后写你要执行的sql语句最后交给sql处理组件处理。处理完后可以设置给对应的变量类以备接下来的使用from Utils.sql_handler import test_env_conn from TestData.customer.customer_test_data import customerData customer_select_sql fselect * from customer where brand_id {customerData.brandId} and is_visitor 0 and email ! and email not like %willdeskvisitor.com% order by create_at desc customer_select_result test_env_conn.select_many_value(sqlcustomer_select_sql) customer_num len(customer_select_result) setattr(customerData, customer_num, customer_num)最后编写yaml用例文件这样一个用例就处理好了title代表用例标题url代表要请求的url链接method是请求方法data是请求参数before_sql是sql文件路径expected是断言值api1: title: 检查顾客列表 url: /api/v1/customer/local/getBrandCustomers method: post data: {pageParam:{page:1,pageSize:100},v:${v}} before_sql: customer/customer_select.py expected: - eq: {$.code : 0} - eq: {$.data.total : ${customer_num}} - eq: {$.data.list[?(.id ${customer_id})].name : ${customer_name}} - eq: {$.data.list[?(.id ${customer_id})].email : ${customer_email}} - eq: {$.data.list[?(.id ${customer_id})].phone : ${customer_phone}} - eq: {$.data.list[?(.id ${customer_id})].location : ${customer_location}} - eq: {$.data.list[?(.id ${customer_id})].timeZone : ${customer_timezone}} - exec: {code : assert len(res.json()[data][list]) ${customer_num}}