Pytest基础框架
注:本文为学习笔记博,已成功搭建并在原基础上做了一些优化,框架源自:https://blog.csdn.net/tester_sc/article/details/118617901
API 接口管理模块
最基础的模块,存放 api 地址,便于日后查找和修改。将用例和配置分离是很有必要的。可以按照系统的模块来分类存放,例如 注册、登录、忘记密码等都可以放在User 类。
User.py
封装:
from common.GetConfig import baseUrl # 获取接口 host
from common.RestClient import RestClient # 发起请求的类
class User(RestClient):
def __init__(self,baseUrl,**kwargs):
super(User,self).__init__(baseUrl,**kwargs)
def login(self,**kwargs):
return self.post("/login",**kwargs)
def register(self,**kwargs):
return self.post("/register",**kwargs)
def userInfo(self,**kwargs):
return self.post("/info",**kwargs)
user = User(baseUrl)
调用:
import pytest
from api.User import user
class TestGetUserInfo:
def test_01(self,login_fixture):
header = {
"Authorization": login_fixture.access_token
}
res = user.userInfo(headers = header)
data = res.json()
assert data['userLevel'] is not None
Common 公共模块
公共调用工具类,例如日志 Logger、发送请求封装类 RestClient、读取数据类 ReadData、获取环境配置类 GetConfig
Logger.py
可以输出或生成运行日志文件,方便问题定位。由 logging 库二次封装而来。
官方的 logging 模块工作流程图如下: 从下图中我们可以看出看到这几种 类型,Logger、LogRecord、Filter、Handler、Formatter。
类型说明:
Logger:日志,暴露函数给应用程序,基于日志记录器和过滤器级别决定哪些日志有效。
LogRecord:日志记录器,将日志传到相应的处理器处理。
Handler :处理器, 将(日志记录器产生的)日志记录发送至合适的目的地。
Filter:过滤器, 提供了更好的粒度控制,它可以决定输出哪些日志记录。
Formatter:格式化器, 指明了最终输出中日志记录的布局。
封装
import logging,time,os
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 LoggerTest:
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(level=logging.DEBUG)
#生成 Handler对象
self.console = logging.StreamHandler()
self.filelogger = logging.FileHandler(filename=self.logname,mode= 'a')
#设置 Handler日志等级
self.console.setLevel(level= logging.ERROR)
self.filelogger.setLevel(level=logging.DEBUG)
#设置 Handler格式
self.formatter = logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s")
self.console.setFormatter(self.formatter)
self.filelogger.setFormatter(self.formatter)
#给自定义日志对象增加 Handler
self.logger.addHandler(self.console)
self.logger.addHandler(self.filelogger)
logger = LoggerTest().logger
if __name__ == '__main__':
logger.debug('This is a customer debug message')
logger.info('This is an customer info message')
logger.warning('This is a customer warning message')
logger.error('This is an customer error message')
logger.critical('This is a customer critical message')
调用
from common.Logger import logger
logger.info("接口请求地址 ==>> {}".format(url))
logger.info("接口请求方式 ==>> {}".format(method))
logger.info("接口响应为 ==>> {}".format(complexjson.dumps(res, indent=4, ensure_ascii=False)))
RestClient.py
发送请求类,由 Request库封装而来,方便 API 类继承后直接发送请求,还可以让任何请求都被记录在 log 中。这里相较原文件添加了接口响应也打印到 log 中。
封装
import requests
import json as complexjson
from common.Logger import logger
class RestClient():
def __init__(self, baseUrl):
self.api_root_url = baseUrl
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("params")
cookies = dict(**kwargs).get("params")
self.request_log(url, method, data, json, params, headers, files, cookies)
if method == "GET":
res = self.session.get(url, **kwargs)
self.response_log(res.json())
return res
if method == "POST":
res = self.session.post(url, data, json, **kwargs)
self.response_log(res.json())
return res
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
if headers is not None:
logger.info("接口请求头 ==>> {}".format(complexjson.dumps(headers, indent=4, ensure_ascii=False)))
if params is not None:
logger.info("接口请求 params 参数 ==>> {}".format(complexjson.dumps(params, indent=4, ensure_ascii=False)))
if data is not None:
logger.info("接口请求体 data 参数 ==>> {}".format(complexjson.dumps(data, indent=4, ensure_ascii=False)))
if json is not None:
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)))
def response_log(self,res):
logger.info("接口响应 ==>> {}".format(complexjson.dumps(res, indent=4, ensure_ascii=False)))
调用
#可在 User.py中看到
ReadFileData.py
可用于读取.ini .yaml .json文件
封装
import yaml
import json
from configparser import ConfigParser
from common.Logger import logger
class MyConfigParser(ConfigParser):
# 重写 configparser 中的 optionxform 函数,解决 .ini 文件中的 键option 自动转为小写的问题
def __init__(self, defaults=None):
ConfigParser.__init__(self, defaults=defaults)
def optionxform(self, optionstr):
return optionstr
class ReadFileData():
def __init__(self):
pass
def load_yaml(self, file_path):
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 load_json(self, file_path):
logger.info("加载 {} 文件......".format(file_path))
with open(file_path, encoding='utf-8') as f:
data = json.load(f)
logger.info("读到数据 ==>> {} ".format(data))
return data
def load_ini(self, file_path):
logger.info("加载 {} 文件......".format(file_path))
config = MyConfigParser()
config.read(file_path, encoding="UTF-8")
data = dict(config._sections)
# print("读到数据 ==>> {} ".format(data))
return data
data = ReadFileData()
调用
#请看 GetConfig.py
GetConfig.py
可用于获取环境配置信息
封装
import os
from common.ReadFileData import data
class GetConfig:
def __init__(self):
self.base_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
self.config_path = os.path.join(self.base_path, "config", "devtr.ini")
self.config_data = data.load_ini(self.config_path)
def setEnvir(self,envir):
self.config_path = os.path.join(self.base_path, "config", envir)
self.config_data = data.load_ini(self.config_path)
def getHost(self):
baseUrl = self.config_data['host']['baseUrl']
return baseUrl
config = GetConfig()
baseUrl = config.getHost()
调用
from requests import Session
from common.GetConfig import baseUrl
from api.User import user
class Login():
def __init__(self):
self.s = Session()
self.baseUrl = baseUrl
def login(self,username,password):
self.username = username
self.password = password
data = {
"user": self.username,
"password": self.password
}
res = user.login(json = data)
r = res.json()
if res.status_code == 200:
self.userId = r['user']
else:
print(r)
def refreshToken(self):
res = user.refreshToken()
r = res.json()
if res.status_code == 200:
self.access_token = r['access_token']
self.refresh_token = r['refresh_token']
else:
print(res)
if __name__ == '__main__':
username = '***'
password = '***'
user = Login(username,password)
try:
print(user.access_token)
print('-------------------')
print(user.refresh_token)
except Exception as e:
print('Login failed,because:')
print(e)
Database.py
执行数据库操作的库
封装
import psycopg2
from common.GetConfig import config
class Database:
def __init__(self):
self.databaseConfig = config.config_data
self.host = self.databaseConfig['database']['HOST']
self.port = self.databaseConfig['database']['PORT']
self.user = self.databaseConfig['database']['USER']
self.passwd = self.databaseConfig['database']['PASSWD']
def connect(self,database):
'''连接串'''
self.conn = psycopg2.connect(host = self.host,port = self.port,database = database,user = self.user,password = self.passwd)
def select(self,sql):
'''开启游标'''
self.cur = self.conn.cursor()
'''执行语句'''
self.cur.execute(sql)
'''获取结果集的每一行'''
rows = self.cur.fetchall()
'''关闭游标'''
self.cur.close()
return rows
def update(self,sql):
'''开启游标'''
self.cur = self.conn.cursor()
'''执行语句'''
self.cur.execute(sql)
'''关闭游标'''
self.cur.close()
'''提交'''
self.conn.commit()
def __exit__(self, exc_type, exc_val, exc_tb):
'''关闭连接'''
self.conn.close()
if __name__ == '__main__':
database = Database()
database.connect('database_name')
sql1 = "***"
sql2 = "***"
database.update(sql1)
rows = database.select(sql2)
for row in rows:
print(row)
调用
from common.Database import Database
database = Database()
database.connect('database_name')
sql1 = "***"
sql2 = "***"
database.update(sql1)
rows = database.select(sql2)
for row in rows:
print(row)