Python+Requests+PyWebIO框架详解,编写测试工具提高团队测试效率

一、背景

老铁们如果是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"
}

def Login(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
    
def YourGetRequests(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


# 工具类
class ApiKey:
    # get请求的封装:因为params可能存在无值的情况,存放默认None
    def get(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也可能存在无值得情况,存放默认None
    def post(self, url, data=None, **kwargs):
        # return requests.post(url=url, data=data, **kwargs)
        return requests.request(url=url, data=data, **kwargs)

    # 基于jsonpath获取数据的关键字:用于提取所需要的内容
    def get_text(self, data, key):
        # jsonpath获取数据的表达式:成功则返回list,失败则返回false
        # loads是将json格式的内容转换为字典的格式
        # jsonpath接收的是dict类型的数据
        dict_data = json.loads(data)
        value = jsonpath.jsonpath(dict_data, key)
        if isinstance(value, list):
            if len(value) == 1:
                value = value[0]
        return value

    # 断言封装
    def my_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


class MyRequests:

    # 初始化方法
    def __init__(self, environment, api):
        # 测试环境
        self.environment = environment.upper()
        # 接口数据
        self.api = api
        self.ak = ApiKey()

    def send_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 >= 200 and 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<

猜你喜欢

转载自blog.csdn.net/weixin_44691253/article/details/131656464