关键字 开发-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条测试用例:

posted @ 2023-11-28 21:08  dack_deng  阅读(247)  评论(0编辑  收藏  举报