一、背景
老铁们如果是QA,想必也遇到过类似痛点吧:
- 业务逻辑复杂性决定测试场景复杂性,配置测试场景常常花费大量时间,导致测试效率降低
- 新用户的测试场景,账号可能经常注销,协助debug时需要用userid,每次都得重新抓包。而且测试账号很多,来回切,即使在本地管理userid,维护成本也很大,不如现用现查来的准确可靠
- 问开发要了一些接口,通过接口配置测试场景,相较于手工配置方便了一些。但如果涉及到多个接口层级调用,配置一个账号还算方便,当配置多个账号时也是特别麻烦,使用Postman就需要编写脚本,用起来我个人感觉还不如Python方便
- 最需要测试工具的是QA,开发和PM也需要但不是很频繁。如果是本地的Python脚本,其他人要用,每次都需要你去帮忙操作。密集测试时,这种操作的需求太多,别人的效率提高了,你的效率反而降低了。。
怎么样可以“一劳永逸”呢?编写出来工具,方便所有人使用?
使用Python+Requests+PyWebIO开发框架,基于这个框架编写测试工具,部署后,团队内部通过链接即可访问,无需配置任何环境(有浏览器就够),可以实现所有人完全 自助 使用,真正地能提高团队效率。
有了这个框架,只要后端有接口,就能开发成工具。再也不用愁某些Feature 回归成本高了。而且仅通过输入手机号就能查询各种信息,简直不要太方便啦。
这个框架熟悉Python的测试同学很快可以上手,自己设计自己开发,怎么方便怎么来,简直太香了。
二、框架设计
1、api
1)存放接口模板,我一般以host命名,此host下的所有接口都存放在此python下。
2、common
1)api_key:get(), post(), jsonpath提取方法的封装。
2)base_func:对常用字段的解析方法进行封装
3)my_requests:用户输入接口参数值后,替换掉模板中的数据,并调用api_key中的get, post方法发送请求
3、config
1)settings:获取文件路径、以及数据库连接信息配置(如有)等
2)VAR:存放常量,例如手机号的白名单list、测试环境的一些选项等
4、debug
1)一个python脚本,可发送post请求,用于debug
5、util
1)get_available_port:部署服务时,查询可用端口号
2)int_redis(如需要):redis的连接、查询以及删除等操作
3)my_logger(如需要):logger的封装,此框架中我实际并未使用到。
4)mysql_client(如需要):mysql的连接、查询以及删除等操作
6、webio
1)common_ui:像下拉菜单、输入框、单选复选框等,可以进行封装。需要时进行调用。也可以将首页作为一个独立的UI进行封装,每个PyWebIO对其进行调用,实现每个工具都可以进入首页。(PyWebIO有提供多application的start方法,但是我觉得没有这种方式好管理)
2)homepage:对所有工具进行一个统一展示,可以是各种你喜欢的形式,我一般用表格输出,工具名称,地址,以及描述,owner等信息。此python需要作为PyWebIO app来部署。
3)tool_x:编写你的测试工具UI以及业务逻辑,py文件可以根据你的业务逻辑进行命名,代码结构中仅是一个示例。此python需要作为PyWebIO app来部署
7、main
1)project根目录下的main.py,执行它之后,它将会收集所有PyWebIO app,并且部署服务
三、框架详解
1、api层
shop_xo.py
1.1 设计说明
- python文件以host进行命名,方便管理
- 一个api python中,存放此host下的所有接口
- 将Host作为全局变量,且根据实际的环境进行区分,方便测试工具在不同环境下的使用。我的项目中仅Int和Prod两个环境。
- headers如果这些接口都一样,可以单独拿出来,不用每个接口都写。demo中的login请求不用填写headers。
- 每一个func都以接口名直接命名(后端大佬接口命名已经很make sense啦,并且这个仅仅是接口模板,并无业务逻辑)
- 接口 func的设计:一个请求所需的全部数据作为一个字典进行存储 (key的命名一定要跟get和post方法的参数名一致)。由于为了知道是int还是prod,所以请求数据中有IntUrl和ProdUrl。最终发送请求如何处理,请见my_requests的设计。
- 对于eval使用不了解的可以看我之前的一篇文章:getattr和eval在Python接口测试中的应用
1.2 代码实现
IntHost ="http://shop-xo.hctestedu.com/index.php?"
ProdHost ="http://shop-xo.hctestedu.com/index.php?"# Headers = {'Content-Type': 'application/json'}
param_data ={
"application":"app","application_client_type":"weixin"}defLogin(accounts, pwd):
api_path ="s=api/user/login"
data ={
"accounts": accounts,"pwd": pwd,"type":"username"}
request_info ={
"IntUrl": IntHost + api_path,"ProdUrl": ProdHost + api_path,"method":"post",# "headers": eval(f'{Headers}'),'data':eval(f'{
data}'),"params": param_data
}return request_info
defYourGetRequests(userid):
api_path =f"/YourGetRequests?userid={
userid}"
request_info ={
"IntUrl": IntHost + api_path,"ProdUrl": ProdHost + api_path,"method":"get","headers":eval(f'{
Headers}')}return request_info
if __name__ =='__main__':print(Login("hello","world"))
2、common层
2.1 api_key
2.1.1 设计说明
- 发送请求,常用的是post、get请求,对其进行封装
- 收到响应后,需要对响应结果进行解析,因此对jsonpath的使用进行封装
- 有时需要用到断言,也可以一并封装
2.1.2 代码实现
import json
import jsonpath
import requests
# 工具类classApiKey:# get请求的封装:因为params可能存在无值的情况,存放默认Nonedefget(self, url, params=None,**kwargs):# return requests.get(url=url, params=params, **kwargs) # 使用这种方式,需要字典中不包含method参数return requests.request(url=url, params=params,**kwargs)# 使用这种方式,需要字典中带有method参数# post请求的封装:data也可能存在无值得情况,存放默认Nonedefpost(self, url, data=None,**kwargs):# return requests.post(url=url, data=data, **kwargs)return requests.request(url=url, data=data,**kwargs)# 基于jsonpath获取数据的关键字:用于提取所需要的内容defget_text(self, data, key):# jsonpath获取数据的表达式:成功则返回list,失败则返回false# loads是将json格式的内容转换为字典的格式# jsonpath接收的是dict类型的数据
dict_data = json.loads(data)
value = jsonpath.jsonpath(dict_data, key)ifisinstance(value,list):iflen(value)==1:
value = value[0]return value
# 断言封装defmy_assert(self, acutal, expect):try:assert acutal == expect
except:return"断言失败"else:return"断言成功"
2.2 my_requests
2.2.1 设计说明
构造方法中的两个成员变量
- environment:用户在使用工具时,选择的环境
- api:在api层,host下的接口func
请求模板中参数的替换
- 将用户输入的数据,作为模板中的实参,在调用接口func时例如,Login(“zz”, “123456”),可以直接替换掉
类方法:get_request_info()
- 根据用户选择的环境,确定请求url,确定后需要删除字典中的IntUrl和ProdUrl,新增url
类方法:send_requests()
- 使用get_request_info()拿到所有请求数据之后,需要对请求method做一个判断,然后调用api_key中的post或者get方法,调用时,使用了getattr。
- 对于getattr使用不了解的,可以看我之前的一篇文章:getattr和eval在Python接口测试中的应用
- 为什么注释掉了del dict_data[“method”],可以结合上面api_key的get和post代码中的注释。使用getattr时,dict中的key需要跟post、get方法中参数名保持一致,不然就会报错,重复或者post、get方法中不存在的参数名都不行。
- 使用dumps将响应体转换成Json格式。并且ensure_ascii设置成False,后面put_code打印响应体时,中文就能正常展示。indent=4,可以让输出为标准的Json格式,可读性更强。
2.2.2 代码实现
import json
from json import JSONDecodeError
from api.shop_xo import Login
from common.api_key import ApiKey
classMyRequests:# 初始化方法def__init__(self, environment, api):# 测试环境
self.environment = environment.upper()# 接口数据
self.api = api
self.ak = ApiKey()defsend_requests(self):global resp
dict_data = self.get_request_info()# dict类型if dict_data["method"]=="post":# del dict_data["method"]
resp =getattr(self.ak,'post')(**dict_data)elif dict_data["method"]=="get":
resp =getattr(self.ak,'get')(**dict_data)if resp.status_code >=200and resp.status_code <300:try:
json_resp = resp.json()# 是字典
json_resp = json.dumps(json_resp, indent=4, ensure_ascii=False)# dumps()将字典转换成Json, loads()将Json转换成字典except JSONDecodeError as e<
版权归原作者 Summer@123 所有, 如有侵权,请联系我们删除。