Python+pytest+request+allure接口自动化
一、脚本结构
二、配置文件
pytest.ini这个文件他是pytest的单元测试框架的核心配置文件
1、位置:项目跟目录
2、编码:必须是ANSI,可以使用notpad++修改编码格式
3、作用:改变pytest默认行为。
4、运行的规则:不管是主函数的模式运行,命令行模式运行,都回去读取这个配置文件
[pytest]
addopts = -vs --alluredir ./temp
testpaths = ./testcases
python_files = test_*.py
项目统一安装模块,放到requirements.txt中,通过pip install -r requirements.txt
验证是否安装成功:pytest -V
pytest
pytest-html (生成htm格式的自动化测试报告)
pytest-xdist 测试用例分布式执行,多cpu分发
pytest-ordering 用户改版测试用例的执行顺序(从上到下)
pytest-rerunfailures 用例失败后重跑
allure-pytest 用于生成美观的测试报告
三、公共模块部分代码
conparator.py
class comparators:
# 判断接口相应码
def assertEqual(self, check, expect):
if check == expect:
assert check == expect
logger.info("断言 ==>> 结果 ==>>通过 ")
return True
else:
logger.info("断言 ==>> 结果 ==>>失败 ")
logger.info("接口返回码是 【 {} 】, 预计接口返回码:{} ".format(check, expect))
return False
# 判断接口返回内容是否包含关键字
def assertIn(self, check, expect):
if check == expect:
assert check in expect
logger.info("断言 ==>> 结果 ==>>通过 ")
return True
else:
logger.info("断言 ==>> 结果 ==>>失败 ")
logger.info("接口关键字是 【 {} 】, 预计接口关键字:{} ".format(check, expect))
return False
logger.py
BASE_PATH = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
# 定义日志文件路径
LOG_PATH = os.path.join(BASE_PATH, "log")
if not os.path.exists(LOG_PATH):
os.mkdir(LOG_PATH)
class Logger:
def __init__(self):
self.logname = os.path.join(LOG_PATH, "{}.log".format(time.strftime("%Y%m%d")))
self.logger = logging.getLogger("log")
self.logger.setLevel(logging.DEBUG)
self.formater = logging.Formatter(
'[%(asctime)s][%(filename)s %(lineno)d][%(levelname)s]: %(message)s')
self.filelogger = logging.FileHandler(self.logname, mode='a', encoding="UTF-8")
self.console = logging.StreamHandler()
self.console.setLevel(logging.DEBUG)
self.filelogger.setLevel(logging.DEBUG)
self.filelogger.setFormatter(self.formater)
self.console.setFormatter(self.formater)
self.logger.addHandler(self.filelogger)
self.logger.addHandler(self.console)
read_yaml.py
class ReadFileYaml:
def __init__(self):
pass
def read_yaml(self, file_path):
"""读取yaml文件"""
logger.info("加载{}文件...........".format(file_path))
with open(file_path, encoding='utf-8') as f:
data = yaml.safe_load(f)
logger.info("读取到的数据===》{}".format(data))
return data
def write_yaml(self, data, file_path):
"""向yaml文件写入数据"""
with open(file_path, encoding='utf-8', mode='w') as f:
return yaml.dump(data, stream=f, allow_unicode=True)
def update_yaml(self, path, key, value):
"""向yaml文件修改数据"""
logger.info("开始修改数据")
doc = self.read_yaml(path)
doc[key] = value
self.write_yaml(doc,path)
logger.info("修改后到的数据===》{}".format(doc))
rest_client.py
class RestClient:
def __init__(self, api_root_url):
self.api_root_url = api_root_url
self.session = requests.session()
def get(self, url, **kwargs):
return self.request(url, "GET", **kwargs)
def post(self, url, data=None, json=None, **kwargs):
return self.request(url, "POST", data, json, **kwargs)
def put(self, url, data=None, **kwargs):
return self.request(url, "PUT", data, **kwargs)
def delete(self, url, **kwargs):
return self.request(url, "DELETE", **kwargs)
def patch(self, url, data=None, **kwargs):
return self.request(url, "PATCH", data, **kwargs)
def request(self, url, method, data=None, json=None, **kwargs):
url = self.api_root_url + url
headers = dict(**kwargs).get("headers")
params = dict(**kwargs).get("params")
files = dict(**kwargs).get("files")
cookies = dict(**kwargs).get("cookies")
self.request_log(url, method, data, json, params, headers, files, cookies)
if method == "GET":
return self.session.get(url, **kwargs)
if method == "POST":
return requests.post(url, data, json, **kwargs)
if method == "PUT":
if json:
# PUT 和 PATCH 中没有提供直接使用json参数的方法,因此需要用data来传入
data = complexjson.dumps(json)
return self.session.put(url, data, **kwargs)
if method == "DELETE":
return self.session.delete(url, **kwargs)
if method == "PATCH":
if json:
data = complexjson.dumps(json)
return self.session.patch(url, data, **kwargs)
def request_log(self, url, method, data=None, json=None, params=None, headers=None, files=None, cookies=None, **kwargs):
logger.info("接口请求地址 ==>> {}".format(url))
logger.info("接口请求方式 ==>> {}".format(method))
# Python3中,json在做dumps操作时,会将中文转换成unicode编码,因此设置 ensure_ascii=False
logger.info("接口请求头 ==>> {}".format(complexjson.dumps(headers, indent=4, ensure_ascii=False)))
logger.info("接口请求 params 参数 ==>> {}".format(complexjson.dumps(params, indent=4, ensure_ascii=False)))
logger.info("接口请求体 data 参数 ==>> {}".format(complexjson.dumps(data, indent=4, ensure_ascii=False)))
logger.info("接口请求体 json 参数 ==>> {}".format(complexjson.dumps(json, indent=4, ensure_ascii=False)))
logger.info("接口上传附件 files 参数 ==>> {}".format(files))
logger.info("接口 cookies 参数 ==>> {}".format(complexjson.dumps(cookies, indent=4, ensure_ascii=False)))
四、逻辑层部分代码
逻辑层代码,使用request二次封装请求接口传入参数。我的几口基本都是post,所以基本每一个api逻辑层都是一样的,就是名字不一样。
from core.rest_client import RestClient
class BasicUserInfo(RestClient):
def __init__(self, api_root_url,**kwargs):
super(BasicUserInfo, self).__init__(api_root_url, **kwargs)
def basic_user_info(self, url, **kwargs):
return self.post(url, **kwargs)
五、操作层
api在请求接口前有什么操作,需要什么动态参数,或者接口关联测试(上一个接口的返回内容,作为这个接口的请求参数),我这里关联接口处理方法是把数据存入yaml文件,然后请求的时候把数据读取出来,这只是我想的方法(可以参考),处理方法很多,根据自身的接口可以做相应的处理。
六、应用层
使用的测试框架是pytest,也可以使用unittest框架,根据自己的要求来,我觉得pytest更加灵活比unittest好用,个人意见勿喷。(这里对失败用例,和跳过用例等,没有做任何处理)
now_file = os.path.abspath(".")
file_path = now_file.replace('\\', '/')
readfileyaml = ReadFileYaml()
case = readfileyaml.read_yaml(file_path + '/data/dim_trend_case.yaml')
@allure.severity(allure.severity_level.NORMAL)
@allure.feature("监控概括数据")
class TestGeneralize:
@pytest.mark.parametrize('data', case)
def test_01_dimension_trend_this_week(self, data):
allure.dynamic.title(data['detail'])
allure.dynamic.description(data['detail'])
# 发起请求
result = get_dimension_trend(data['url'], data['data'], data['headers'])
# 断言判断
asse = comparators().assertEqual(str(result['code']), str(data['resp']['code']))
if not asse:
pytest.xfail(reason="这个用例实际返回结果与预期一致")
七、启动文件
run.py
if __name__ == "__main__":
pytest.main()
os.system('allure generate ./temp -o ./report --clean-alluredir')
# 我这里的cookies十二个小时过期,一般自动化脚本一天执行一次,我执行完了就把cookie清楚了
open(file_path + "/common/cookies.txt", 'w').close()