关键字 开发-06 封装yaml文件直接生成测试用例
前言
前面几个章节,我们主要是如何通过yaml文件的数据自动转换成测试用例,并按照关键字去执行,如下是之前我们通过参数化的形式手动执行用例。
from utils.run import RunByKey
# 获取文件路径
file_path = Path(__file__).parent.joinpath('data', 'login.yml')
@pytest.mark.parametrize('key, value', read_file.read_yaml(file_path)['data'])
def test_login(key, value, base_url):
print(f"测试用例名称: {value['name']}")
print(f"测试数据: {value['request']}") # data ---dict
print(f"base_url: {base_url}")
s = HttpSession(base_url=base_url)
run = RunByKey(s, read_file.read_yaml(file_path)['config'])
run.run(value)
现在我们继续封装,不进行手动参数化方式执行用例。也就是我们连上面的代码也无需写,只通过yaml文件直接执行用例。
如果认真点,我们就会发现,在05章节中的最后一张图中,也就是我们动态生成测试用例函数的时候,我们会发现一个问题:如下所示:
1.Python经典问题
上面的问题我们叫做python经典问题,如何去解决这个问题呢?
首先我们举个例子,来分析下这个问题形成的过程:
res = []
data = {"testa": "aaa", "testb": "bbb"}
for key, value in data.items():
def fun():
print(value)
res.append(fun)
print(res)
for f in res:
f()
打印结果:
[<function fun at 0x0000020D5DC76280>, <function fun at 0x0000020D5DDFC280>]
bbb
bbb
分析:
1.for 循环data里面的数据,函数在循环内部定义,但是未调用
2.在for循环内部将函数对象添加到res列表中
3.for循环调用函数f(),而此时的value值取最新的为第二个“bbb”,于是打印2个bbb
1.1 解决函数对象调用后,内部值被覆盖的问题
解决思路,我们首先需要确定函数被谁调用, 然后知道被谁调用后单独取出这个函数对应的值。
import inspect
import inspect
def fun():
# 被谁调用
call_function_name = inspect.getframeinfo(inspect.currentframe().f_back)[2]
print(call_function_name)
return "33"
def fun_x():
fun()
def fun_y():
fun()
fun_x()
fun_y()
打印:
fun_x
fun_y
于是我们在执行yaml的函数中,添加这个方法:
from pytest import Module, Function
import types
import yaml
from utils.run import RunYaml
import inspect
from utils.create_function import create_function_from_parameters
from inspect import Parameter
def pytest_collect_file(file_path, parent):
# 查找test开头的文件
if file_path.suffix in ['.yml', '.yaml'] and (file_path.name.startswith('test') or file_path.name.endstartswith('test')):
print(f'yaml文件路径:{file_path}')
print(f'yaml文件路径名称:{file_path.stem}')
# 构造 pytest 框架的 Module,module由Package构建,Package由系统构建
py_module = Module.from_parent(parent, path=file_path)
# 动态创建测试module模块(.py文件),这里才能给下面的_getobj进行导入
MyModule = types.ModuleType(file_path.stem)
from utils.read_file import read_yaml
from pathlib import Path
file_path = Path(__file__).parent.joinpath('data', 'login.yml')
raw_data = read_yaml(file_path)['data']
for key, value in raw_data:
def foo(arg):
print(f"执行的参数: {arg}")
call_function_name = inspect.getframeinfo(inspect.currentframe().f_back)[2]
raw_data_dict = dict(raw_data)
print(f"执行的内容: {raw_data_dict[call_function_name]}")
f = create_function_from_parameters(func=foo,
parameters=[Parameter('request', Parameter.POSITIONAL_OR_KEYWORD)],
documentation="some doc",
func_name=key,
func_filename="main.py")
# 向 module 中加入test 函数
setattr(MyModule, key, f)
# 重写_getobj,返回自己定义的Mymodule
py_module._getobj = lambda: MyModule
return py_module
2. 修改功能模块,完成yaml文件直接生成测试用例
修改conftest.py文件
# conftest.py
from pytest import Module, Function
import types
import yaml
from utils.run import RunYaml
def pytest_collect_file(file_path, parent):
# 查找test开头的文件
if file_path.suffix in ['.yml', '.yaml'] and (file_path.name.startswith('test') or file_path.name.endstartswith('test')):
print(f'yaml文件路径:{file_path}')
print(f'yaml文件路径名称:{file_path.stem}')
# 构造 pytest 框架的 Module,module由Package构建,Package由系统构建
py_module = Module.from_parent(parent, path=file_path)
# 动态创建测试module模块(.py文件),这里才能给下面的_getobj进行导入
MyModule = types.ModuleType(file_path.stem)
raw_dict = yaml.safe_load(file_path.open(encoding='utf-8'))
print(f'读取到的yaml raw:{raw_dict}')
# 生成用例,执行用例
runner = RunYaml(raw_dict, MyModule)
runner.run()
# 重写_getobj,返回自己定义的Mymodule
py_module._getobj = lambda: MyModule
return py_module
将run.py文件修改如下,class RunBykey改成class RunYaml:
"""利用反射运行关键字"""
import jinja2
import json
import inspect
from types import ModuleType
from requests import Response
from utils import my_builtins
from utils.create_function import create_function_from_parameters
from inspect import Parameter
class RunYaml:
def __init__(self, raw: dict, module: ModuleType):
self.raw = raw
self.context = {} # 变量容器
self.module = module
@staticmethod
def name(value):
print(f'用例名称:{value}')
def request(self, request_data: dict):
print(f"执行request: {request_data}")
res = self.session.send_request(**request_data)
return res
def run(self):
# 1.先获取到config中的变量variables
if not self.raw.get('config'):
self.raw['config'] = {}
base_url = self.raw.get('config').get('base_url', None) # 获取config中base_url
config_variables = self.raw.get('config').get('variables',{})
print('获取到的config中的变量:', config_variables)
# 2.先渲染config_variables
# self.context.update(config_variables), 这个时候就需要先渲染再更新到变量容器中
self.context.update(__builtins__)
self.context.update(my_builtins.__dict__) # 更新内置函数和my_builtins 内置变量、函数
t1 = jinja2.Template(json.dumps(config_variables),
variable_start_string='${',
variable_end_string='}')
self.module_variables = json.loads(t1.render(**self.context)) # --> dict
# 3.更新到变量容器中
if isinstance(self.module_variables, dict):
self.context.update(self.module_variables)
case = {} # 收集用例名称和执行内容
for case_name, case_value in self.raw.items():
print(f"用例执行内容case_value: {case_value}")
if case_name == 'config':
continue # 跳过config内容,非用例部分
if not str(case_name).startswith('test'):
case_name = 'test_' + str(case_name)
if isinstance(case_value, list): # 把测试用例放到用例容器case中
case[case_name] = case_value
else:
case[case_name] = [case_value] # 以list的形式加入到用例容器中case中
def execute_yaml_case(args):
"""执行yaml 中用例部分,根据这个函数动态生成其他测试用例函数"""
print(f"执行的参数: {args}")
# 被谁调用
call_function_name = inspect.getframeinfo(inspect.currentframe().f_back)[2]
print('执行的内容: ', self.raw[call_function_name])
for item , value in self.raw[call_function_name].items():
# 根据关键字去执行
if item == 'name':
pass
elif item == 'print':
print(value)
elif item == "request":
print(f"发送request 请求: {value}")
f = create_function_from_parameters(func=execute_yaml_case,
parameters=[Parameter('request', Parameter.POSITIONAL_OR_KEYWORD)],
documentation="some doc",
func_name=case_name,
func_filename=f"{self.module.__name__}.py")
# 向 module中加入test 函数
setattr(self.module, case_name, f)
2.1 完善requests请求封装
在requests请求这里增加异常处理,这样遇到问题,我们可以捕获异常,方便问题定位。
首先我们新增自定义异常函数:
# utils/exceptions.py
class ParserError(Exception):
pass
class ExtractExpressionError(Exception):
pass
class ConnectTimeout(Exception):
pass
class MaxRetryError(Exception):
pass
class ConnectError(Exception):
pass
# utils/http_session.py
import re
import requests
import urllib3
from requests import Response
from . import exceptions
def send_request(self, method, url, base_url=None, **kwargs) -> Response:
"""
发送request请求
:param method:
:param url:
:param base_url:
:param kwargs:
:return: Response
"""
url = self.check_url(base_url, url) if base_url else self.check_url(self.base_url, url)
try:
return self.request(method, url, timeout=self.timeout, verify=False, **kwargs)
except requests.exceptions.ConnectTimeout as msg:
print(f'{method} {url} --> {str(msg)}')
except urllib3.exceptions.MaxRetryError as msg:
print(f'{method} {url} --> {str(msg)}')
raise exceptions.MaxRetryError(msg) from None
except requests.exceptions.ConnectionError as msg:
print(f'{method} {url} --> {str(msg)}')
raise exceptions.ConnectError(msg) from None
# 你可以使用"from"关键字指定原因。在这种情况下,原因被设定为None
2.2 增加测试用例全局session会话
# conftest.py
import pytest
from pytest import Module, Function
import types
import yaml
from utils.http_session import HttpSession
from requests.adapters import HTTPAdapter
from utils.run import RunYaml
from utils import exceptions
@pytest.fixture(scope='session')
def requests_session(request):
"""全局session 全部用例仅执行一次"""
s = HttpSession()
# max_retries=2 重试2次
s.mount('http://', HTTPAdapter(max_retries=2))
s.mount('https://', HTTPAdapter(max_retries=2))
yield s
s.close()
然后我们在conftest.py文件中的钩子函数:pytest_collect_file
,加个异常处理,我们只允许以test开头或者结尾的yaml或者yaml文件。
def pytest_collect_file(file_path, parent):
# 查找test开头的文件
try:
if file_path.suffix in ['.yml', '.yaml'] and (file_path.name.startswith('test') or file_path.name.endstartswith('test')):
......
except:
raise exceptions.YamlNameError('yml or yaml file is not start or end with test') # 不是test开头或者结尾的yml文件,抛出异常。
2.3 生成测试函数时添加会话参数和base_url
# utils/run.py/fun函数
def execute_yaml_case(args):
"""执行yaml 中用例部分,根据这个函数动态生成其他测试用例函数"""
print(f"执行的参数: {args}")
# 被谁调用
call_function_name = inspect.getframeinfo(inspect.currentframe().f_back)[2]
print('执行的内容: ', self.raw[call_function_name])
for item , value in self.raw[call_function_name].items():
# 根据关键字去执行
if item == 'name':
pass
elif item == 'print':
print(value)
elif item == "request":
print(f"发送request 请求: {value}")
request_session = args.get('requests_session')
BASE_URL = base_url if base_url else args.get('base_url') # 判断yml中是否传入base_url,得到全局base_url
response = request_session.send_request(base_url=BASE_URL, **value)
print(f'执行结果:{response.text}')
f = create_function_from_parameters(func=execute_yaml_case,
parameters=[
Parameter('request', Parameter.POSITIONAL_OR_KEYWORD),
Parameter('requests_session', Parameter.POSITIONAL_OR_KEYWORD), # 新增的requests_session测试用例前置fixture
Parameter('base_url', Parameter.POSITIONAL_OR_KEYWORD), # 新增全局base_url的fixture
],
documentation=case_name,
func_name=case_name,
func_filename=f"{self.module.__name__}.py")
# 向 module中加入test 函数
setattr(self.module, case_name, f)
# data/test_login.yml
config:
name: yaml申明变量
variables:
username: "admin"
password: "Admin@22"
email1: ${email1}
test_login:
name: 登录成功
request:
url: /api/v1/auth/login
method: POST
json:
username: ${username}
password: ${password}
test_login2:
name: 登录失败
request:
url: /api/v1/auth/login
method: POST
json:
username: ${email1}
password: ${email2()}
终端执行:pytest .\data\test_login.yml -s
,执行成功,生成2条测试用例:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律