登录接口信息
新增接口信息
修改接口信息
删除接口信息
查询接口信息
测试用例存储字段
CREATE TABLE `test_case_list` (# 测试用例的编号,不为空,自增长
`id` INT ( 0 ) NOT NULL AUTO_INCREMENT,# 项目名称
`web` VARCHAR ( 255 ) DEFAULT NULL,# 项目模块
`module` VARCHAR ( 255 ) DEFAULT NULL,# 测试用例的标题
`title` VARCHAR ( 255 ) DEFAULT NULL,# 接口地址的路径
`url` VARCHAR ( 255 ) DEFAULT NULL,# 请求方法
`method` VARCHAR ( 255 ) DEFAULT NULL,# 请求头
`headers` VARCHAR ( 255 ) DEFAULT NULL,# cookies 秘钥
`cookies` VARCHAR ( 1000 ) DEFAULT NULL,# 请求主体信息
`request_body` VARCHAR ( 1000 ) DEFAULT NULL,# 请求主体的数据类型
`request_type` VARCHAR ( 255 ) DEFAULT NULL,# 关联
`relation` VARCHAR ( 255 ) DEFAULT NULL,# 预期业务状态码
`expected_code` VARCHAR ( 255 ) DEFAULT NULL COMMENT ' 作为
断言标准 ',# 测试用例是否可运行
`isdel` INT ( 0 ) NULL DEFAULT 1 COMMENT '0 为删除, 1 为正常',# 设置 id 为主键
PRIMARY KEY ( `id` ) USING BTREE # 设置表的引擎为 InnoDB
) ENGINE = INNODB;
配置信息存储字段
CREATE TABLE `test_config` (# 配置信息序号
`id` INT ( 0 ) NOT NULL,# 项目名称
`web` VARCHAR ( 255 ) DEFAULT NULL,# 环境信息字段
`key` VARCHAR ( 255 ) DEFAULT NULL,# 环境信息的值
`value` VARCHAR ( 255 ) DEFAULT NULL,# 设置 id 为主键
PRIMARY KEY ( `id` ) USING BTREE # 设置表的引擎为 InnoDB
) ENGINE = INNODB;
执行结果记录存储字段
CREATE TABLE `test_result_record` (# 执行结果记录的序号,不为空,自增长
`id` INT ( 0 ) UNSIGNED NOT NULL AUTO_INCREMENT,# 被执行测试用例的 id
`case_id` VARCHAR ( 255 ) DEFAULT NULL,# 执行结果更新的时间
`times` VARCHAR ( 255 ) DEFAULT NULL,# 程序运行的实际结果
`response` VARCHAR ( 1000 ) DEFAULT NULL COMMENT '实际结果',# 用例执行是否通过
`result` VARCHAR ( 255 ) DEFAULT NULL,# 设置 id 为主键
PRIMARY KEY ( `id` ) USING BTREE # 设置表的引擎为 InnoDB
) ENGINE = INNODB;
框架层次结构
common
base.py
import json
from string import Template
import re
# 根据参数匹配内容
def find(data):
# 判断data类型是否为字典
if isinstance(data, dict):
# 对象格式化为str
data = json.dumps(data)
# 定义正则匹配规则
pattern = "\\${(.*?)}"
# 匹配进行查询,把查询的结果返回
return re.findall(pattern, data)
# 进行参数替换
def replace(ori_data, replace_data):
# 对象格式化为str
ori_data = json.dumps(ori_data)
# 处理字符串的类,实例化并初始化原始字符
s = Template(ori_data)
# 使用新的字符,替换
return s.safe_substitute(replace_data)
# 根据var,逐层获取json格式的值
def parse_relation(var, resdata):
# 判断变量var是否存在
if not var:
# 不存在,直接返回resdata内容
return resdata
else:
# 存在,获取数组第一个内容
resdata = resdata.get(var[0])
# 从数组中删除第一个内容
del var[0]
# 递归
return parse_relation(var, resdata)
if __name__ == '__main__':
ori_data = {"admin-token": "${token}"}
replace_data = {'token': 'x015k878'}
print(replace(ori_data, replace_data))
config
settings.py
import os
# 获取文件绝对路径
abs_path = os.path.abspath(__file__)
# 文件所在目录的上一级目录,获取根目录
project_path = os.path.dirname(os.path.dirname(abs_path))
# sep获取config目录的全路径
_conf_path = project_path + os.sep + "config"
# sep获取log目录的全路径
_log_path = project_path + os.sep + "log"
# sep获取report目录的全路径
_report_path = project_path + os.sep + "report"
# 数据库配置信息
DB_CONFIG = {
"host": "192.168.70.130",
"user": "root",
"password": "123456",
"database": "test",
"charset": "utf8",
"port": 33506
}
# 返回日志目录
def get_log_path():
return _log_path
# 返回报告目录
def get_report_path():
return _report_path
# 返回config目录
def get_config_path():
return _conf_path
# test_run执行中,实现动态设置,获取属性,实现关联变量
class DynamicParam:
pass
if __name__ == '__main__':
print(get_config_path())
log
report
report.html
testcase
test_run.py
import datetime
from config.settings import DynamicParam
from utils.logutil import logger
import common.base as Base
import json
import pytest
from utils.readmysql import RdTestcase
from utils.requestsutil import RequestSend
# 初始化类
attribute = DynamicParam()
# 实例化测试用例对象
case_data = RdTestcase()
# 根据测试用例对象获取测试用例列表
case_list = case_data.is_run_data('zrlog')
# 获取当前时间
current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# 测试用例执行类
class TestApi:
# 类方法,运行前只执行一次
def setup_class(self):
# 日志
logger.info(f"----开始执行测试用例,开始时间为:{current_time}----")
# 结束后只执行一次
def teardown_class(self):
logger.info(f"----测试用例执行完毕,完成时间为:{current_time}----")
# 测试用例参数化
@pytest.mark.parametrize('case', case_list)
def test_run(self, case):
# 定义变量
res_data = None
# 根据条件,从数据库获取url信息,然后拼接完整url信息
url = case_data.loadConfkey('zrlog', 'url_api') ['value'] + case['url']
# 获取method内容
method = case['method']
# 获取headers内容,格式化字符为字典
headers = eval(case['headers'])
# 获取cookies内容,格式化字符为字典
cookies = eval(case['cookies'])
# 获取请求内容,格式化字符问字典
data = eval(case['request_body'])
# 获取关联内容
relation = str(case['relation'])
# 获取测试用例名称
case_name = case['title']
# 根据关联获取headers参数中是否有变量被替换,有则替换,无则默认
headers = self.correlation(headers)
# 根据关联获取cookies参数中是否有变量被替换,有则替换,无则默认
cookies = self.correlation(cookies)
# 根据关联获取data参数中是否有变量被替换,有则替换,无则默认
data = self.correlation(data)
# 异常处理
try:
# 日志
logger.info("正在执行{}用例".format(case_name))
# 执行测试用例,发送HTTP请求
res_data = RequestSend().send(url, method, data=data, headers=headers, cookies=cookies)
# 日志
logger.info("用例执行成功,请求的结果为{}".format(res_data))
except:
# 日志
logger.info("用例执行失败,请查看日志找原因。")
# 断言结果为失败
assert False
# 判断res_data是否存在
if res_data:
# res_data存在后,判断relation不为None
if relation != "None":
# 设置变量token的值为响应结果的信息
self.set_relation(relation, res_data)
# 对结果进行验证
self.assert_respoes(case, res_data)
return res_data
# 响应结果关联设置函数
def set_relation(self, relation, res_data):
try:
# 判断relation内容为True
if relation:
# 根据,进行分割,结果为list
relation = relation.split(",")
# 循环打印relation列表
for i in relation:
# 根据=进行分割
var = i.split("=")
# 列表第一个值设置为var_name
var_name = var[0]
# 列表第二个值内容按.进行分割,内容保存到变量var_tmp
var_tmp = var[1].split(".")
# 响应结果在res_data中,根据var_tmp进行匹配
res = Base.parse_relation(var_tmp, res_data)
# 打印
print(f"{var_name}={res}")
# 把定义的变量名称和值以属性的方式设置到DynamicParam类中,实现动态存储
setattr(DynamicParam, var_name, res)
except Exception as e:
print(e)
# 根据关联,获取该变量内容
def correlation(self, data):
# 根据正则,获取数据
res_data = Base.find(data)
# 判断1res_data为True
if res_data:
# 定义空的字典
replace_dict = { }
# 循环打印
for i in res_data:
# 根据名称,从DynamicParam动态获取属性值,并把内容赋值给变量data_tmp
data_tmp = getattr(DynamicParam, str(i), "None")
# 把结果更新到字典replace_dict中
replace_dict.update({str(i): data_tmp})
data = json.loads(Base.replace(data, replace_dict))
return data
# 结果验证方法
def assert_respoes(self, case, res_data):
# 变量初始化为False
is_pass = False
try:
# 根据结果进行断言验证
assert int(res_data['body']['error']) == int(case['expected_code'])
# 日志
logger.info("用例断言成功")
# 设置变量为True
is_pass = True
except:
# 设置变量为False
is_pass = False
# 日志
logger.info("用例断言失败")
finally:
# 把结果更新到数据库
case_data.updateResults(res_data, is_pass, str(case['id']))
# 根据结果是T还是F进行断言
assert is_pass
return is_pass
if __name__ == '__main__':
pytest.mark(['-s', '-v', 'test_run.py'])
utils
logutil.py
from config.settings import get_log_path
import logging
import time
import os
# 控制日志是否输出
STREAM = True
class LogUtil:
def __init__(self):
# 初始化日志对象,设置日志名称
self.logger = logging.getLogger("logger")
# 设置总体日志级别
self.logger.setLevel(logging.DEBUG)
# 避免日志重复
if not self.logger.handlers:
# 定义日志名称
self.log_name = '{}.log'.format(time.strftime("%Y_%m_%d", time.localtime()))
# 定义日志路径及文件名称
self.log_path_file = os.path.join(get_log_path(), self.log_name)
# 定义文件处理handler
fh = logging.FileHandler(self.log_path_file, encoding='utf-8', mode='w')
# 设置文件处理handler的日志级别
fh.setLevel(logging.DEBUG)
# 日志格式变量
formatter = logging.Formatter("%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s")
# 设置打印格式
fh.setFormatter(formatter)
# 添加handler
self.logger.addHandler(fh)
# 关闭handler
fh.close()
# 控制台输出
if STREAM:
# 定义控制台输出流handler
fh_stream = logging.StreamHandler()
# 输出日志级别
fh_stream.setLevel(logging.DEBUG)
# 设置打印格式
fh_stream.setFormatter(formatter)
# 添加handler
self.logger.addHandler(fh_stream)
def log(self):
# 返回定义好的logger对象,对外直接使用
return self.logger
# 其他程序可直接调用logger对象
logger = LogUtil().log()
# 测试
if __name__ == '__main__':
logger.info('test')
mysqlutil.py
import pymysql
from config.settings import DB_CONFIG
from utils.logutil import logger
class MysqlUtil:
def __init__(self):
# 初始化pymysql数据库连接+创建游标
self.db = pymysql.connect(**DB_CONFIG)
self.cursor = self.db.cursor(cursor=pymysql.cursors.DictCursor)
# 单条数据
def get_fetchone(self, sql):
# 执行sql
self.cursor.execute(sql)
# 查询单条数据,返回结果
return self.cursor.fetchone()
# 多条数据
def get_fetchall(self, sql):
# 执行sql
self.cursor.execute(sql)
# 查询多条数据,返回结果
return self.cursor.fetchall()
# 执行更新类sql
def sql_execute(self, sql):
try:
# db和游标同事存在
if self.db and self.cursor:
# 执行sql,insert或update操作
self.cursor.execute(sql)
self.db.commit()
except Exception as e:
# 出异常回滚
self.db.rollback()
logger.error("sql执行错误,已执行回滚操作")
return False
# 关闭对象
@staticmethod
def close(self):
# 游标是否存在,存在则关闭指针
if self.cursor is not None:
self.cursor.close()
# 数据库对象是否存在,存在则关闭
if self.db is not None:
self.db.close()
# 测试
if __name__ == '__main__':
mysql = MysqlUtil()
res = mysql.get_fetchall("select * from test_case_list")
print(res)
readmysql.py
import datetime
import json
from utils.mysqlutil import MysqlUtil
from utils.logutil import logger
# 初始化mysql工具类
mysql = MysqlUtil()
class RdTestcase:
# 加载所有的测试用例
def load_all_case(self, web):
sql = f"select * from test_case_list where web = '{web}'"
# 调取工具类方法,获取所有数据
results = mysql.get_fetchall(sql)
return results
# 筛选可以执行的测试用例
def is_run_data(self, web):
# 根据条件isdel==1筛选可执行的测试用例
run_list = [case for case in self.load_all_case(web) if case['isdel'] == 1]
return run_list
# 获取配置信息
def loadConfkey(self, web, key):
# 根据web, key查询test_config相关配置信息
sql = f"select * from test_config where web = '{web}' and `key` = '{key}'"
# 调取方法查询1条结果
results = mysql.get_fetchone(sql)
return results
# 更新测试结果
def updateResults(self, response, is_pass, case_id):
# 获取当前时间
current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# 更新测试用例执行结果,插入test_result_record表
sql = f"insert into test_result_record (case_id,times,response,result) values ('{case_id}','{current_time}','{json.dumps(response, ensure_ascii=False)}','{is_pass}')"
# 执行insert
rows = mysql.sql_execute(sql)
logger.debug(sql)
# 返回True/False
return rows
if __name__ == '__main__':
test = RdTestcase()
res = test.updateResults(
{'code': 200,
'body': {
'error': 1,
'message': '用户名和密码都不能为空'},
'cookies': {}
},
'True', '4565'
)
print(res)
requestutil.py
import requests
from utils.logutil import logger
class RequestSend:
# 封装requests请求函数
def api_run(self, url, method, data=None, headers=None, cookies=None):
# 定义变量,获取响应结果
res = None
# 打印日志
logger.info("请求的url为{},类型为{}".format(url, type(url)))
logger.info("请求的method为{},类型为{}".format(method, type(method)))
logger.info("请求的data为{},类型为{}".format(data, type(data)))
logger.info("请求的headers为{},类型为{}".format(headers, type(headers)))
logger.info("请求的cookies为{},类型为{}".format(cookies, type(cookies)))
# 判断请求方法
if method == "get":
res = requests.get(url, data=data, headers=headers, cookies=cookies)
elif method == "post":
# 判断请求的数据类型是否是json
if headers == {"Content-Type": "application/json"}:
# 发送HTTP请求,方法为post,参数使用json=data
res = requests.post(url, json=data, headers=headers, cookies=cookies)
elif headers == {"Content-Type": "application/x-www-form-urlencoded"}:
# 发送HTTP请求,方法为post,参数使用data=data
res = requests.post(url, data=data, headers=headers, cookies=cookies)
# 获取请求响应的状态码
code = res.status_code
# 获取请求响应的cookies
cookies = res.cookies.get_dict()
# 定义字典
dict1 = dict()
# 异常处理
try:
# 获取响应结果json格式
body = res.json()
except:
# 获取响应结果text
body = res.text
# 自定义参数写入字典
dict1['code'] = code
dict1['body'] = body
dict1['cookies'] = cookies
return dict1
# 对外调用 **kwargs传入的参数是dict类型
def send(self, url, method, **kwargs):
# 调用自定义方法
return self.api_run(url=url, method=method, **kwargs)
if __name__ == '__main__':
url = "http://192.168.70.130/admin/login"
data = {
"username": "admin",
"password": "b53685e590d5a7c71a24ebc592b49bf0",
"https": False,
"key": 1655363186867
}
method = "post"
headers = {"Content-Type": "application/json"}
print(RequestSend().send(url=url, method=method, headers=headers, data=data))
pytest.ini
[pytest]
addopts = -s -v --html=../report/report.html
testpaths = testcase
python_files = test_*.py
python_classes = Test*
python_functions = test*