高效自动化测试框架-优秀实践02-接口
高效实践点
-
编写接口的操作的时候只需要编写接口的url,请求方法,请求体的样例
-
其他的将接口封装成服务或者关键字的操作,全部使用装饰器来封装,能做到高效的解耦
-
在表示层编写业务测试用例的时候,可以使用函数式的编程方式,非常易读,还非常易于copy,提升编写效率
问题背景
-
业务测试用例编写完成之后,需要很多时间去调试,常常是针对入参和返回值做处理
-
业务测试用例很不整齐,即代码没有对齐,也不能直观看出脚本的行为,需要仔细读取函数的名称,并进一步理解
-
业务脚本的函数命名不规范,不统一,导致不易于理解和修改
-
关键字中常常由很多重复的代码,比如接收入参并填充进入请求体中,对于返回值有常常需要根据json格式去取出响应数据
-
最开始的框架,提供了很多功能,很灵活,但是多就是少,功能越多越灵活就会导致学习成本和维护成本直线上升,还不如直接统一格式
-
关键字中的逻辑是多种多样的,如果需要维护,有可能需要对很多个关键字去维护,耗费时间巨大
解决思路
-
Api接口的封装,分成4个操作方式去封装,即增删查改
-
每个操作行为封装一个接口函数,并且接口中只包含URL,请求体,请求方法
-
对于接口的行为,全部由统一的装饰器去封装,分别针对请求体为json,urlencode,xml,html等格式去封装,默认是json
-
对于接口行为的封装,将入参装载进入请求体的方法统一封装,即自动寻找对应的数据,然后填充到请求体中去
-
对于接口行为的封装,需要提取返回值的时候,上层传入目标值名称,和对应的正则表达式(jsonpath),然后获得对应的数据
相关代码
接口代码示例
from core.logic import Api @Api.json def add_goods(goodsSn="", name="", **kwargs):req_method = "POST"url = "admin/goods/create"body_data = {"goods": {"picUrl": "","gallery": [],"isHot": False,"isNew": True,"isOnSale": True,"goodsSn": "9001","name": None},"specifications": [{"specification": "规格","value": "标准","picUrl": ""}],"products": [{"id": 0,"specifications": ["标准"],"price": "66","number": "66","url": ""}],"attributes": []}return req_method, url, body_data @Api.json def rmv_goods(id="", **kwargs):req_method = "POST"url = "admin/goods/delete"body_data = {"id": None}return req_method, url, body_data def lst_goods(name="", **kwargs):req_method = "GET"url = "admin/goods/list"body_data = {"name": "","order": "desc","sort": "add_time"}return req_method, url, body_data def dtl_goods(id="", **kwargs):req_method = "GET"url = "admin/goods/detail"body_data = {"id": None}return req_method, url, body_data
用于封装接口函数,并提供入参填充,返回值提取,和日志打印功能的的装饰器
E:\Develop\LoranTest\core\logic.py
import jsonpath import functools import json from core.base_api import BaseApi from core.logger.logger_interface import logger class RequestData:class KeyError(Exception):def __init__(self, error_key):error_dict = {"find_too_many_key": "The key value is incorrect. The request data contains at least two keys named $key. ""Please modify the incoming key name, that is, $parent key + $key","can_not_find_key": "The key you entered could not be found in the dictionary",}self.error_info = error_dict[error_key] def __str__(self):return repr(self.error_info) class FindKeyError(Exception):def __str__(self):return repr("The key you entered could not be found in the dictionary") def __init__(self):self.data = Noneself.out_data = None def set_data(self, json_dict):self.data = json_dictself.out_data = self.data def iter_node(self, rows, road_step, target):if isinstance(rows, dict):key_value_iter = (x for x in rows.items())elif isinstance(rows, list):key_value_iter = (x for x in enumerate(rows))else:returnfor key, value in key_value_iter:current_path = road_step.copy()current_path.append(key)if key == target:yield current_pathif isinstance(value, (dict, list)):yield from self.iter_node(value, current_path, target) def find_one(self, key: str) -> list:path_iter = self.iter_node(self.data, [], key)for path in path_iter:return pathreturn [] def find_all(self, key: str) -> list:path_iter = self.iter_node(self.data, [], key)return list(path_iter) def _edit_one_path(self, paths: list, value):alias_of_data = self.out_datafor path in paths[0:-1]:alias_of_data = alias_of_data[path]alias_of_data[paths[-1]] = value def change(self, key: str, value):if "_" not in key:res = self.find_all(key)if len(res) > 1:raise self.KeyError("find_too_many_key")paths = res[0]self._edit_one_path(paths, value)else:key_list = key.split("_")res = self.find_all(key_list[-1])for temp in key_list:if temp not in res[0]:raise self.KeyError("can_not_find_key")paths = res[0]self._edit_one_path(paths, value)pass return self.out_data def modify(self, json_dict, **kwargs):out_data = json_dictfor key, value in kwargs.items():self.set_data(out_data)out_data = self.change(key, value)return out_data class ResponeseData: def fetch_one_value(self, data, var_info):var_name = var_info[0]json_path_reg = var_info[1]value = jsonpath.jsonpath(data, json_path_reg)[0]return value def fetch_all_value(self, data, fetch_info):# TODO 这里有可能存在一个问题,只对单个调教的信息提取做处理,未对多条件的进行处理for info in fetch_info:return self.fetch_one_value(data, info) class Api:@classmethoddef json(self, func):@functools.wraps(func)def wrapper(*args, **kwargs):"""我是 wrapper 的注释"""# 提取fetch入参fetch = Noneif "fetch" in kwargs.keys():fetch = kwargs["fetch"]del kwargs['fetch'] res = func(*args, **kwargs)logger.debug(func.__name__ + "::kwargs: " + json.dumps(kwargs)) req_method, url, body_data = resreq_body = RequestData().modify(body_data, **kwargs)logger.debug(func.__name__ + "::req_body: " + json.dumps(req_body)) req_api = BaseApi(role="admin")rsp_data = req_api.send(method=req_method, url=url, json=req_body)logger.debug(func.__name__ + "::req_body: " + json.dumps(rsp_data)) # 针对fetch入参做处理if fetch:fetch_var = ResponeseData().fetch_all_value(rsp_data, fetch)logger.debug(func.__name__ + "::req_body: " + json.dumps(fetch_var))return wrapper def form(self):pass def urlencoded(self):pass def binary(self):pass def test(self):pass def js(self):pass def html(self):pass def xml(self):pass
接口的请求的基类
E:\Develop\LoranTest\core\base_api.py
import json import requests from core.logger.logger_interface import logger from config.environment import Environment class BaseApi:def __init__(self, role=None):env = Environment()self.base_url = env.base_urlself.token = Noneself.role = role def _get_token(self, role=None):if role != "admin" and role != "client":raise ValueErrorurl = {"admin": "admin/auth/login","client": "wx/auth/login",}data = {"admin": {"username": "admin123", "password": "admin123"},"client": {"username": "user123", "password": "user123"},}req_token = {"admin": "X-Litemall-Admin-Token","client": "X-Litemall-Token",}req = requests.request("post", self.base_url + url[role], json=data[role])self.token = {req_token[role]: req.json()["data"]["token"]}pass def _set_token(self, request_infos):if self.token is None:self._get_token(role=self.role) if request_infos.get("headers"):request_infos["headers"].update(self.token)else:request_infos["headers"] = self.tokenreturn request_infos def send(self, method="", url="", **kwargs):kwargs = self._set_token(kwargs)rsp = requests.request(method, self.base_url + url, **kwargs)rsp_json = rsp.json()logger.debug(f"BaseApi::send ==> {url}接口的响应为{json.dumps(rsp_json, indent=2, ensure_ascii=False)}")return rsp_json
待改进的地方
-
返回值目前仅支持返回一条数据
-
请求的积累中,需要重复去获取token信息,速度慢,以后可以改成直接读取redis的方式去实现
-
查询某类资源的时候,需要用变量去接收返回值,代码格式还不够统一,可以考虑使用传入类变量的方式去实现,直接接收返回值的方式,并且Python在作用于这块限制的比较严格
项目地址
GitHub - WaterLoran/LoranTest