一、前言
想想之前玩的框架,做的项目都是把数据用例冗余到一起的,不符合数据用例分离的思想,所以准备基于pytest搭建个测试框架,支持数据用例分离,接下来会用一系列文章逐步介绍整个框架是怎么搭建起来的。
二、项目结构概览
- conf:存放接口路径、域名等信息
- data:1、存放.json文件(接口的请求头或请求体信息),即测试数据;2、存放.yaml文件(用户信息 比如账号密码)
- log:日志文件
- report:allure测试报告
- testcase:测试用例
- tools:常用方法
- conftest.py:用于处理登录的文件
- excute.py:用于执行项目的文件
三、照葫芦画瓢
1、新建python项目后,建个testcase文件夹
/testcase下面创建个case,比如test_case1.py,里面随意写些内容,主要用于调试excute.py的执行是否正常,要注意的是.py文件要用test开头,因为用的是pytest框架。
2、新建excute.py,源码如下
这里我就不演示了,执行成功后会在report文件夹下面生成测试报告
# -*- coding:utf-8 -*- ''' @Date:2022/10/5 20:30 @Author:一加一 ''' import pytest import os if __name__ == '__main__': # pytest执行脚本并生成测试结果文件到report/tmp目录下 pytest.main(['-s','--alluredir','report/tmp']) # 将report/tmp目录下的结果文件生成html类型的测试报告文件到report/html目录下 # -o report/html --clean 是为了清空已有的测试报告再生成 os.system(r'allure generate report/tmp -o report/html --clean')
Pytest接口测试框架实战项目搭建(二)—— tools公共方法封装
一、前言
在项目中我们要频繁地用到log日志、request请求方法、断言等,所以我们可以把这些常用的方法封装成日志,下面将逐个文件讲述下,不会讲太细,但会把源码贴出来,还有一点要说的是公共方法如果看不懂的话可以不用纠结,知道用处以及清楚在实际业务中怎么调用公共方法就行了。
二、tools目录展示
公共方法封装不会涉及到业务代码。
三、common.py
该文件需要导入封装好的日志方法,因为在接口请求时要把日志打印出来,后续脚本有问题可方便排查,tools.logger在下面会贴源码
知识点一:注意.json()转换,根据实际业务需求进行封装
''' @Date:2022/10/1 8:18 @Author:一加一 ''' import requests from tools.logger import log # 导入封装的logger方法 class Common: '''封装通用接口''' #request-post请求,适用Content-Type:application/x-www-form-urlencoded,即表单传参,类似some=data&xxx=xxx 的形式 def r_post_form(url,headers,data): log.info("请求参数为:{},{}".format(url,data)) res = requests.post(url,headers=headers,data=data).json() log.info("响应结果为:{}".format(res)) return res #request-post请求,适用'Content-Type': 'application/json',即json传参 def r_post(url,headers,json): log.info("请求参数为:{},{}".format(url, json)) res = requests.post(url,headers=headers,json=json).json() log.info("响应结果为:{}".format(res)) return res # request-post请求,适用'Content-Type': 'application/json',即json传参——请求时不转换成json,适配s统一登录系统接口请求 def r_s_post(url, headers, json): log.info("请求参数为:{},{}".format(url, json)) res = requests.post(url, headers=headers, json=json) log.info("响应结果为:{}".format(res.text)) return res #request-get请求 def r_get(url,heasers,json): log.info("请求参数为:{},{}".format(url, json)) res = requests.get(url,headers=heasers,json=json) log.info("响应结果为:{}".format(res)) return res #断言:s统一登录系统的message=操作成功 def assert_s_code_message(res_json): code = res_json['code'] message = res_json['message'] assert code == '200' and message == '操作成功' # 断言:业务系统的message=请求成功 def assert_tg_code_message(res_json): code = res_json['code'] message = res_json['message'] assert code == '200' and message == '请求成功'
四、logger.py
1、logger.py源码如下
知识点一:
应该大多数的日志都是用的内置的logging库,但是该框架使用的是loguru库,所以需要安装下 pip3 install loguru
知识点二:
1.os.path.abspath 作用: 获取当前脚本的完整路径
2.os.path.dirname 功能:去掉文件名,返回目录
3.os.path.join() 连接 两个或更多的路径名组件
知识点三:
.format():用于格式化方法,即用来控制字符串和变量的显示效果,增强了字符串格式化的功能
''' @Date:2022/9/30 20:49 @Author:一加一 ''' # -*- coding:utf-8 -*- from loguru import logger from datetime import datetime from conf.setting import * class Logger: ''' loguru封装日志记录器 ''' def __new__(cls, *args, **kwargs): ''' 1.os.path.abspath 作用: 获取当前脚本的完整路径 2.os.path.dirname 功能:去掉文件名,返回目录 3.os.path.join() 连接两个或更多的路径名组件 :param args: :param kwargs: :return: ''' log_name = datetime.now().strftime("%Y-%m-%d") # 以时间命名日志文件,格式为"年-月-日" sink = os.path.join(LOG_PATH,"{}.log".format(log_name)) # 日志记录文件路径 level = "DEBUG" # 记录的最低日志级别为DEBUG encoding = "utf-8" # 写入日志文件时编码格式为utf-8 enqueue = True # 多线程多进程时保证线程安全 rotation = "500MB" # 日志文件最大为500MB,超过则新建文件记录日志 retention = "1 week" # 日志保留时长为一星期,超时则清除 logger.add( sink=sink, level=level, encoding=encoding, enqueue=enqueue, rotation=rotation, retention=retention ) return logger log = Logger()
2、直接执行该文件,会在log文件夹生成一个.log文件,如下图
3、该文件需要导入目录文件设置,即conf.setting,主要用于读取LOG_PATH
''' @Date:2022/10/3 20:56 @Author:一加一 ''' import os '''管理文件存放路径''' BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) REPORT_PATH = os.path.join(BASE_PATH,'report') #报告存放的目录 CASE_PATH = os.path.join(BASE_PATH,'testcase') #测试用例的目录 CASE_DATA_PATH = os.path.join(BASE_PATH,'data') #测试数据的目录 LOG_PATH = os.path.join(BASE_PATH,"log") CONFIG_FILE = os.path.join(BASE_PATH,'conf','config.ini') #配置文件的目录 LOGIN_DATA_YAML_FILE = os.path.join(BASE_PATH, 'data', 'userInfo.yaml') #配置文件的目录
五、operate_config.py
1、该文件主要是读取配置文件,即读取conf/config.ini,这里先贴上operate_config.py源码,后面用到该方法会提及
读取配置文件主要是为了适配公司的多个测试环境,如若有需要切换环境,则直接改配置文件即可
# -*- coding:utf-8 -*- import yaml import configparser from conf.setting import CONFIG_FILE,LOGIN_DATA_YAML_FILE def get_yaml(goal): with open(LOGIN_DATA_YAML_FILE, encoding='utf-8') as f: yaml_log = yaml.load(f, Loader=yaml.FullLoader) goal_list = yaml_log.keys() if goal in goal_list: return yaml_log[goal] else: print('不存在的配置') class OperateConfig: def __init__(self): self.config = configparser.ConfigParser() # 调用外部的读取配置文件的方法 self.config.read(CONFIG_FILE, encoding='GBK') def get_node_value(self,node, name): value = self.config.get(node, name) return value def set_node_value(self,section,node,name): """写入配置文件""" self.config.set(section,node,name) # 修改指定section 的option self.config.write(open(CONFIG_FILE, 'w'))
2、conf/config.ini源码如下
因为涉及到公司敏感信息,所以用xxx代替了,配置文件主要存储登录系统和业务系统的接口域名
[ENV] env = QA1 [QA1] y_api_url = https://qa1-api.y.cn s_api_url = https://qa-s-xxx.cn [QA2] y_api_url = https://qa2-api.y.cn s_api_url = https://qa-s-xxx.cn
六、operate_json.py
1、该文件主要封装对.json文件的读取或修改操作,用于接口请求时要读取请求体或请求头,又或者往请求体里插入变量字段,源码如下
''' @Date:2022/10/2 8:18 @Author:一加一 ''' import json import os from conf.setting import CASE_DATA_PATH data_path = os.path.join(CASE_DATA_PATH, "test.json") class OperationJson: def __init__(self, file_names=None): if file_names: self.file_name = file_names else: self.file_name = data_path def open_json(self): """打开json文件 :return:返回json文件数据 """ with open(self.file_name, 'r',encoding='utf-8') as fp: data = json.load(fp) return data fp.close() def key_get_data(self, key1, key2=None): """通过key值获取数据 :param key1: :param key2: :return: """ if key2 is None: data = self.open_json()[key1] else: data = self.open_json()[key1][key2] return data def write_data(self, w_data, key1, key2=None): """修改json数据 :param w_data: 修改后的数据 :param key1: 要修改的键值1 :param key2: 要修改的键值2 :return: """ data_dict = self.open_json() if key2 is None: data_dict[key1] = w_data else: data_dict[key1][key2] = w_data with open(self.file_name, 'w',encoding='utf-8') as fp: fp.write(json.dumps(data_dict, ensure_ascii=False, sort_keys=False, indent=2)) # 对写入的json数据进行格式化 fp.close() def write_datas(self, w_data, key1, key2,key3=None): """修改json数据 :param w_data: 修改后的数据 :param key1: 要修改的键值1 :param key2: 要修改的键值2 :param key3: 要修改的键值3 :return: """ data_dict = self.open_json() if key3 is None and key2 is None: data_dict[key1] = w_data elif key3 is None and key2 is not None: data_dict[key1][key2] = w_data else: data_dict[key1][key2][key3] = w_data with open(self.file_name, 'w',encoding='utf-8') as fp: fp.write(json.dumps(data_dict, ensure_ascii=False, sort_keys=False, indent=2)) # 对写入的json数据进行格式化 fp.close() if __name__ == "__main__": #file_name = "test.json" a = OperationJson() b = a.key_get_data("login_header") #print(type(b)) print(b)
2、文件里加了调试代码,直接执行该文件后效果如下图
调试代码说明:
1)定义数据路径变量
data_path = os.path.join(CASE_DATA_PATH, "test.json")
2)文件作为脚本直接执行,调用OperationJson的key_get_data方法获取test.json里名为“login_header”的josn串数据
if __name__ == "__main__": a = OperationJson() b = a.key_get_data("login_header") print(b)