测试平台执行引擎开发笔记(CLY)
一、测试引擎架构设计
1、执行引擎方案
-
接入测试工具,如:Jmeter、MeterSphere
-
利用第三方自动化框架,如:httprunner(很多培训机构都是采用此方案)
-
用python自己封装
2、测试执行引擎架构设计
二、测试数据设计
1、单个测试用例
"""
====================用例数据格式设计====================
用例名称:XXX
接口信息:
接口地址
请求方法
请求头信息:
请求参数信息:
查询参数
请求体:
json格式
表单格式
文件上传
前置脚本:
随机生成测试数据
查询数据库
定义变量
后置脚本:
断言
提取数据
定义变量
--------------------------------------------
请求库: requests or httpx
示例:requests.post(url, data, params, json, files...)
用例数据中的变量,存放在ENV中:
${{变量名}}
"""
case = {
"title": '',
"interface": {
"url": '/system-api/system/auth/login',
"method": "post"
},
"headers": {
"Content-Type": "application/json"
},
"request": {
"params": {},
"json": {
"username": "test1",
"password": "admin123",
},
"data": {},
},
"setup_script": '',
"teardown_script": '',
}
2、测试环境
"""
====================测试环境格式设计====================
环境名称:XXX
基础url:
请求头:
全局变量
调式变量
全局工具函数
数据库配置:
"""
ENV = {
"name": '',
"base_url": 'http://172.30.1.88:38180/',
"headers": {},
"global_vars": {
"username": 'test1',
"password": 'admin123'
},
"debug_vars": {},
"func_tools": '',
"db": [
{
"name": "local",
"type": "mysql",
"config": {
"host": "127.0.0.1",
"port": 3306,
'user': 'root',
'password': '123456',
'database': 'test001'
}
},
{
"name": "bmos",
"type": "mysql",
"config": {
"host": "172.30.1.88",
"port": 3306,
'user': 'root',
'password': '不告诉你',
'database': 'test002'
}
}
],
}
三、测试执行基类设计
1、测试执行基类搭建
"""
=======================测试执行基类设计V1.0====================
1、执行前置脚本
2、发送请求
3、执行后置脚本
4、执行入口
"""
class BaseCase:
def __setup_script(self, data):
# 执行前置脚本
pass
def __teardown_script(self, data, response):
# 执行后置脚本
def __send_request(self, data):
# 发送请求
def perform(self, data):
# 记录当前用例数据
self.data = data
# 执行前置脚本
self.__setup_script(data)
# 发送请求
response = self.__send_request(data)
# 执行后置脚本
self.__teardown_script(data, response)
2、测试执行基类——测试数据处理
"""
=======================测试执行基类设计V2.0====================
1、用例数据初始化
2、用例数据格式组装(符合requests库的格式)
3、用例数据正则替换
"""
class BaseCase:
def __init__(self, env):
# 获取每个用例的环境数据
self.env = env
def __replace_data(self, request_data):
# 正则替换 ${{变量名}}
pattern = r'\${{(.+?)}}'
request_data = str(request_data)
while re.search(pattern, request_data):
match = re.search(pattern, request_data)
key = match.group(1)
value = self.env.get('global_vars', {}).get(key, '')
request_data = request_data.replace(match.group(), str(value))
return eval(request_data)
def __handle_requests_data(self, data):
# 处理请求数据
# 1-处理method
request_data = {'method': data['interface']['method'].upper()}
# 2-处理url
url = data['interface']['url']
if not url.startswith('http'):
base_url = self.env.get('base_url').rstrip('/')
url = f'{base_url}/{url.lstrip("/")}'
request_data['url'] = url
# 3-处理headers
request_data['headers'] = {**self.env.get('headers', {}), **data.get('headers', {})}
# 4-处理查询参数
request = data['request']
request_data['params'] = request.get('params', {})
# 5-处理请求体参数
content_type = request_data['headers'].get('Content-Type', '').lower()
if content_type == 'application/json':
request_data['json'] = request.get('json')
self.request_body = request_data['json']
elif content_type == 'application/x-www-form-urlencoded':
request_data['data'] = request.get('data')
self.request_body = request.get('data')
elif 'multipart/form-data' in content_type:
request_data['files'] = request.get('files')
self.request_body = request.get('files')
# 调用正则替换变量的方法
request_data = self.__replace_data(request_data)
return request_data
def __send_request(self, data):
# 发送请求
# 1-处理请求数据
request_data = self.__handle_requests_data(data)
# 2-发送请求
response = requests.request(**request_data)
# 3-返回响应结果
return response
def ...(其他方法不变)
3、测试执行基类——前后置脚本执行
class BaseCase:
...
def __run_script(self, data):
# 在前后置脚本执行环境中,内置一些变量
test = self
# 获取环境变量
global_vars = self.env['global_vars']
# 获取前置脚本数据(str)
setup_script = data.get("setup_script", '')
# 执行前置脚本
exec(setup_script)
# 等待后续执行
response = yield
# 获取后置脚本数据(str)
teardown_script = data.get("teardown_script", '')
# 执行后置脚本
exec(teardown_script)
yield
def __setup_script(self, data):
# 执行前置脚本
# 获得一个脚本生成器
self.script_hook = self.__run_script(data)
# 执行脚本直到yield
next(self.script_hook)
def __teardown_script(self, data, response):
# 执行后置脚本
# 发送响应结果并执行脚本到yield
self.script_hook.send(response)
# 删除生成器
delattr(self, 'script_hook')
def ...
4、测试执行基类——响应结果提取
class BaseCase:
...
def re_extract(self, obj, ext):
# 正则提取
if not isinstance(obj, str):
obj = str(obj)
res = re.search(ext, obj)
value = res.group(1) if res else ''
return value
def json_extract(self, obj, ext):
# jsonpath提取》单个数据
res = jsonpath(obj, ext)
value = res[0] if res else ''
return value
def json_extract_list(self, obj, ext):
# jsonpath提取》列表数据
res = jsonpath(obj, ext)
value = res if res else []
return value
def ...
5、测试执行基类——断言封装
class BaseCase(HandleLogData):
...
def assertion(self, method, expect, actual):
# 断言
# 断言类型函数映射
method_map = {
"等于": lambda x, y: x == y,
"不等于": lambda x, y: x != y,
"大于": lambda x, y: x > y,
"大于等于": lambda x, y: x >= y,
"小于": lambda x, y: x < y,
"小于等于": lambda x, y: x <= y,
"包含": lambda x, y: x in y,
"不包含": lambda x, y: x not in y,
}
# 获取断言函数
assert_func = method_map.get(method, None)
if not assert_func:
print('不支持的断言方法:', method)
return
else:
print('断言方法:', method)
try:
assert assert_func(actual, expect)
except AssertionError:
raise AssertionError('断言失败,预期结果:', expect, '实际结果:', actual)
else:
print('断言成功,预期结果:', expect, '实际结果:', actual)
def ...
6、测试执行基类——环境变量封装
class BaseCase(HandleLogData):
...
def save_global_vars(self, name, value):
# 保存到环境变量
self.env['global_vars'][name] = value
def del_global_vars(self, name):
# 从环境变量删除
del self.env['global_vars'][name]
def ...
四、其他工具封装
1、日志管理器
class HandleLogData:
def save_log(self, level, msg):
# 将所有输出的log信息,保存到 self.lob_data 属性中
if not hasattr(self, 'log_data'):
setattr(self, 'log_data', [])
getattr(self, 'log_data').append((level, msg, datetime.now()))
print((level, msg, datetime.now()))
def print_log(self, *args):
msg = ' '.join(str(i) for i in args)
self.save_log(level='Print', msg=msg)
def debug_log(self, *args):
msg = ' '.join(str(i) for i in args)
self.save_log(level='Debug', msg=msg)
def info_log(self, *args):
msg = ' '.join(str(i) for i in args)
self.save_log(level='Info', msg=msg)
def warning_log(self, *args):
msg = ' '.join(str(i) for i in args)
self.save_log(level='Warning', msg=msg)
def error_log(self, *args):
msg = ' '.join(str(i) for i in args)
self.save_log(level='Error', msg=msg)
def critical_log(self, *args):
msg = ' '.join(str(i) for i in args)
self.save_log(level='Critical', msg=msg)
======================================================在测试执行基类中使用==============================================
class BaseCase(HandleLogData):
# 继承日志处理类,在基类中需要输出保存的东西,调用父类的 self.xxx_log(*args) 方法
...
2、数据库管理器
class BaseDB:
# 数据库基类
conn = None
cursor = None
def execute(self, sql, args=None):
# 执行SQL语句,返回一行数据
try:
self.cursor.execute(sql, args)
return self.cursor.fetchone()
except Exception as e:
raise e
def execute_list(self, sql, args):
# 执行SQL语句,返回所有行数据
try:
self.cursor.execute(sql, args)
return self.cursor.fetchall()
except Exception as e:
raise e
def close(self):
# 关闭数据库链接
self.cursor.close()
self.conn.close()
class OracleDB(BaseDB):
# Oracle数据库
def __init__(self, db_config):
self.conn = oracledb.connect(**db_config, autocommit=True)
self.cursor = self.conn.cursor(pymssql.cursors.DictCursor)
class SqlServerDB(BaseDB):
# sqlserver数据库
def __init__(self, db_config):
self.conn = pymssql.connect(**db_config, autocommit=True)
self.cursor = self.conn.cursor(pymssql.cursors.DictCursor)
class MysqlDB(BaseDB):
# mysql数据库
def __init__(self, db_config):
self.conn = pymysql.connect(**db_config, autocommit=True)
self.cursor = self.conn.cursor(pymysql.cursors.DictCursor)
class DBClient:
"""数据库链接工具"""
def init_connection(self, dbs):
if isinstance(dbs, dict):
self.create_db_connection(dbs)
elif isinstance(dbs, list):
for db in dbs:
if isinstance(db, dict):
self.create_db_connection(db)
else:
raise TypeError('数据库配置格式错误')
else:
raise TypeError('数据库配置格式错误')
def create_db_connection(self, db: dict):
# 获取数据库链接对象,保存为属性
if not (db.get('name') and db.get('type') and db.get('config')):
raise TypeError('数据库配置格式错误')
if db.get('type') == 'mysql':
setattr(self, db.get('name'), MysqlDB(db.get('config')))
# elif db.get('type') == 'sqlserver':
# setattr(self, db.get('name'), SqlServerDB(db.get('config')))
# elif db.get('type') == 'oracle':
# setattr(self, db.get('name'), OracleDB(db.get('config')))
def close_db_connection(self):
# 关闭数据链接
for db in self.__dict__:
if isinstance(self.__dict__[db], BaseDB):
self.__dict__[db].close()
======================================================在测试执行基类中使用==============================================
class BaseCase(HandleLogData):
...
def perform(self, data):
# 创建数据库链接对象
db = DBClient()
# 初始化数据库链接
db.init_connection(self.env_data['db'])
# 执行发送请求相关其他操作
...
# 关闭数据库链接
db.close_db_connection()
def ...
3、全局工具函数管理
======================================================创建一个工具函数py文件==============================================
# 文件名:functools_demo.py
fk = Faker(locale='zh-CN')
def random_name():
return fk.name()
def ...(定义各种函数)
==========================================创建一个py文件(作为存放全局工具函数的命名空间)=======================================
handle_func.py
======================================================在测试执行基类中使用=================================================
import handle_func as func_tools
# 将内置函数加载到工具函数命名空间
exec(ENV['func_tools'], func_tools.__dict__) # ENV的数据参考:测试数据设计-测试环境
class BaseCase(HandleLogData):
...
======================================================在前后置脚本中使用=================================================
name=func_tools.random_name()
4、测试数据说明
======================================================用例数据:前后置脚本=================================================
case = {
"title": '',
"interface": {
"url": '/system-api/system/auth/login',
"method": "post"
},
"headers": {
"Content-Type": "application/json"
},
"request": {
"params": {},
"json": {
"username": "${{username}}", # 这里会自动进行正则替换
"password": "${{password}}", # 这里会自动进行正则替换
},
"data": {},
},
"setup_script": open(setup_script_path, 'r', encoding='utf-8').read(), # 前后置脚本都是str格式,比如读取某个txt文件
"teardown_script": open(teardown_script_path, 'r', encoding='utf-8').read(), # 前后置脚本都是str格式,比如读取某个txt文件
}
======================================================前后置脚本示例=================================================
# 文件名:setup_script_demo.txt
print("执行前置脚本")
print('当前变量:', global_vars)
name=func_tools.random_name()
idno = func_tools.random_idno()
test.save_global_vars('name', name)
test.save_global_vars('idno', idno)
======================================================环境数据:全局工具函数=================================================
ENV = {
...
"func_tools": open(file_path, 'r', encoding='utf-8').read(),
...
=======================================================全局工具函数示例=================================================
from datetime import datetime, timedelta
from faker import Faker
fk = Faker(locale='zh-CN')
def random_name():
return fk.name()
def random_idno():
return fk.ssn()
def now_datetime(add=None, sub=None):
fmt = "%Y-%m-%d %H:%M:%S"
if add is not None:
return (datetime.now() + timedelta(seconds=add)).strftime(fmt)
elif sub is not None:
return (datetime.now() - timedelta(seconds=sub)).strftime(fmt)
return datetime.now().strftime(fmt)
五、测试套件
以上执行单条用例足够,但实际工作中往往需要一次运行多个测试场景,接下来考虑再写个测试套件
1、测试套件方案
- 使用pytest或unittest
- 用python写
2、测试数据(包含多个测试场景)
cases = [
{
"name": "演示的用例集",
"cases": [
{
"title": '',
"interface": {
"url": '/system-api/system/auth/login',
"method": "post"
},
"headers": {
"Content-Type": "application/json",
},
"request": {
"params": {},
"json": {
"username": "${{username}}",
"password": "${{password}}",
},
"data": {},
},
"setup_script": open(setup_script_path, 'r', encoding='utf-8').read(),
"teardown_script": open(teardown_script_path, 'r', encoding='utf-8').read(),
}, {
"title": '',
"interface": {
"url": '/system-api/donor/create-donor',
"method": "post"
},
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer ${{token}}"
},
"request": {
"params": {},
"json": {
"marriage": 0,
"outJob": 0,
"buildMethod": 2,
"recruitCode": [],
"address": "四川省营山县消水镇巴岩村9组",
"name": "${{name}}",
"nation": 1,
"sex": 1,
"idNoImageFront": "/bmos-v2/system/idCardPic/ID_CARD_76593e00a98c4900850e2385900e2c7b_1711010241512.png",
"idNoAddress": "四川省营山县消水镇巴岩村9组",
"issueOffice": "营山县公安局",
"birth": "1996-09-29",
"idNo": "${{idno}}",
"idNoValidityPre": "2017-07-20",
"idNoValidityAfter": "2027-07-20",
"validType": 2,
"regionCodes": [
{
"code": "510000000000",
"regionName": "四川省",
"pCode": "000000000000"
},
{
"code": "511300000000",
"regionName": "南充市",
"pCode": "510000000000"
},
{
"code": "511322000000",
"regionName": "营山县",
"pCode": "511300000000"
},
{
"code": "511322109000",
"regionName": "消水镇",
"pCode": "511322000000"
}
],
"faceUrl": "/bmos-v2/system/facePic/FACE_PIC_296f7d46d1eb4bd7a7fc4be22830a884_1711010245200.png",
"similarity": 71,
"age": 27,
"donateBloodCheck": "通过",
"addressCommunity": "511322109002",
"areaCode": "511322109002",
"education": 1,
"job": 1,
"phone": "15111111111",
"donorBindReqVO": {
"userRecruiterBindCreate": [],
"userRecruiterBindUpdate": [],
"userRecruiterBindDelete": []
}
},
"data": {},
},
"setup_script": open(setup_script_path2, 'r', encoding='utf-8').read(),
"teardown_script": open(teardown_script_path2, 'r', encoding='utf-8').read(),
},
]
}, {
"name": "演示的用例集2",
"cases": []
}
]
3、测试结果记录器
class TestResulter:
def __init__(self, case_count, name='调试运行'):
self.all = case_count
self.success = 0
self.error = 0
self.fail = 0
self.name = name
# 保存结果
self.result = []
def add_success(self, test: BaseCase):
"""
:param test: 用例信息,BaseCase
:return:
"""
self.success += 1
info = {
'name': getattr(test, 'title', ''),
'url': getattr(test, 'url', ''),
'method': getattr(test, 'method', ''),
'status': 'success',
'status_code': getattr(test, 'status_code', ''),
'request_headers': getattr(test, 'request_headers', ''),
'response_headers': getattr(test, 'response_headers', ''),
'request_body': getattr(test, 'request_body', ''),
'response_body': getattr(test, 'response_body', ''),
'log_data': getattr(test, 'log_data', '')
}
self.result.append(info)
def add_error(self, test: BaseCase, error):
test.error_log('用例执行错误,错误信息如下:')
test.error_log(error)
self.error += 1
info = {
'name': getattr(test, 'title', ''),
'url': getattr(test, 'url', ''),
'method': getattr(test, 'method', ''),
'status': 'error',
'status_code': getattr(test, 'status_code', ''),
'request_headers': getattr(test, 'request_headers', ''),
'response_headers': getattr(test, 'response_headers', ''),
'request_body': getattr(test, 'request_body', ''),
'response_body': getattr(test, 'response_body', ''),
'log_data': getattr(test, 'log_data', '')
}
self.result.append(info)
def add_fail(self, test: BaseCase):
self.fail += 1
info = {
'title': getattr(test, 'title', ''),
'url': getattr(test, 'url', ''),
'method': getattr(test, 'method', ''),
'status': 'fail',
'status_code': getattr(test, 'status_code', ''),
'request_headers': getattr(test, 'request_headers', ''),
'response_headers': getattr(test, 'response_headers', ''),
'request_body': getattr(test, 'request_body', ''),
'response_body': getattr(test, 'response_body', ''),
'log_data': getattr(test, 'log_data', '')
}
self.result.append(info)
def get_result(self):
if self.success == self.all:
state = 'success'
elif self.error > 0:
state = 'error'
else:
state = 'fail'
return {
'name': self.name,
'all': self.all,
'success': self.success,
'fail': self.fail,
'error': self.error,
'result': self.result,
'state': state,
}
4、测试执行器
# 定义存放环境数据的变量
BaseEnv = {}
class TestRunner(object):
def __init__(self, cases, env_data):
# 传入测试数据和环境数据
self.cases = cases
self.env_data = env_data
self.results = []
def run(self):
global BaseEnv
# 创建数据库链接对象
db = DBClient()
# 初始化数据库链接
db.init_connection(self.env_data['db'])
# 遍历测试用例
for items in self.cases:
# 每次执行测试套件前清理环境变量,保证每个测试套件的环境变量都只是初始环境变量
BaseEnv.clear()
BaseEnv = copy.deepcopy(self.env_data)
# 将内置函数加载到工具函数命名空间
exec(BaseEnv['func_tools'], func_tools.__dict__)
print('测试流名称(测试套件):', items['name'])
# 每个测试套件创建一个测试结果记录器
case_count = len(items['cases'])
resulter = TestResulter(case_count=case_count, name=items['name'])
for case in items['cases']:
self.perform(case, resulter, BaseEnv)
# 获取执行结果
result = resulter.get_result()
self.results.append(result)
# 断开数据库链接
db.close_db_connection()
return self.results
def perform(self, case, resulter, BaseEnv):
c = BaseCase(BaseEnv)
# 执行单条测试用例
try:
c.perform(case)
except AssertionError as e:
resulter.add_fail(c)
except Exception as e:
resulter.add_error(c, e)
else:
resulter.add_success(c)
if __name__ == '__main__':
results = TestRunner(cases=cases, env_data=ENV).run()
六、其他
1、说明
因为这个引擎主要为测试平台开发,所以用例数据写起来很大一堆,实际使用时,到时候会在页面填写接口测试用例,自动保存到数据库,然后通过数据库读出来
相对于apifox:
1、解决数据同步问题,方便维护使用管理
2、支持定时任务
3、支持定制化扩展功能
…
环境部署后续写笔记