HttpRunner3.x 源码解析(3)-main_make生成用例文件

main_make

当终端输入httprunner make 目录/文件名,则调用main_make来生成py文件格式的测试用例

HttpRunner3.x 源码解析(3)-main_make生成用例文件_第1张图片

 对于tests_path中的路径,首先进行路径兼容。

如果不是绝对路径,则转换为绝对路径。

然后调用__make()函数,将测试用例文件转为可运行的pytest文件。

def main_make(tests_paths: List[Text]):
    if not tests_paths:
        return []

    ga_client.track_event("ConvertTests", "hmake")

    for tests_path in tests_paths:
        tests_path = ensure_path_sep(tests_path)#路径兼容
        if not os.path.isabs(tests_path): #不是绝对路径
            tests_path = os.path.join(os.getcwd(), tests_path) #转换为绝对路径



        try:
            __make(tests_path)
        except exceptions.MyBaseError as ex:
            logger.error(ex)
            sys.exit(1)

    #格式化pytest文件
    pytest_files_format_list = pytest_files_made_cache_mapping.keys()
    format_pytest_with_black(*pytest_files_format_list)

    return list(pytest_files_run_set)

 pytest_files_format_list返回py文件列表,它的类型是

如:

dict_keys(['D:\\Project\\demo\\testcases\\login_test.py'])

dict.keys()方法是Python的字典方法,它将字典中的所有键组成一个可迭代序列并返回。

(12条消息) Python dict keys方法:获取字典中键的序列_TCatTime的博客-CSDN博客

>>> test_dict = {'Xi\'an':'Shaanxi', 'Yinchuan':'Ningxia'}
>>> test_dict
{"Xi'an": 'Shaanxi', 'Yinchuan': 'Ningxia'}
>>> test_dict.keys()
dict_keys(["Xi'an", 'Yinchuan'])
>>> type(test_dict.keys())

 

*pytest_files_format_list 输出的是dict_keys里单独的值

如D:\Project\demo\testcases\login_test.py D:\Project\demo\testcases\query_test.py

format_pytest_with_black 格式化pytest文件


def format_pytest_with_black(*python_paths: Text):
    logger.info("format pytest cases with black ...")
    try:
        if is_support_multiprocessing() or len(python_paths) <= 1:
            subprocess.run(["black", *python_paths])
        else:
            logger.warning(
                "this system does not support multiprocessing well, format files one by one ..."
            )
            [subprocess.run(["black", path]) for path in python_paths]
    except subprocess.CalledProcessError as ex:
        capture_exception(ex)
        logger.error(ex)
        sys.exit(1)
    except OSError:
        err_msg = """
missing dependency tool: black
install black manually and try again:
$ pip install black
"""
        logger.error(err_msg)
        sys.exit(1)

 首先判断系统是否支持多线程 或者需要格式化的文件只有1个,如果是的话,调用

subprocess.run(["black", *python_paths]),否则,就分别拿出路径中的文件run

这个函数最终返回的,是整合后的py文件列表

return list(pytest_files_run_set)

HttpRunner3.x 源码解析(3)-main_make生成用例文件_第2张图片 

__make()

传入目录或者文件列表,输出 pytest_files_run_set,它是py文件的集合。

def __make(tests_path: Text):
    """ make testcase(s) with testcase/testsuite/folder absolute path
        generated pytest file path will be cached in pytest_files_made_cache_mapping

    Args:
        tests_path: should be in absolute path

    """
    logger.info(f"make path: {tests_path}")
    test_files = []
    if os.path.isdir(tests_path):#如果是目录,则先load
        files_list = load_folder_files(tests_path)
        test_files.extend(files_list)
    elif os.path.isfile(tests_path): #如果是文件,直接加入test_files
        test_files.append(tests_path)
    else:
        raise exceptions.TestcaseNotFound(f"Invalid tests path: {tests_path}")

    for test_file in test_files:
        if test_file.lower().endswith("_test.py"):#文件以_test.py结尾,说明是py文件,不用转换,直接加入py文件集
            pytest_files_run_set.add(test_file)
            continue

        try:
            test_content = load_test_file(test_file) #加载测试用例文件,返回的是json/yaml文件中的内容
        except (exceptions.FileNotFound, exceptions.FileFormatError) as ex:
            logger.warning(f"Invalid test file: {test_file}\n{type(ex).__name__}: {ex}")
            continue

        if not isinstance(test_content, Dict):#判断内容是否为dict
            logger.warning(
                f"Invalid test file: {test_file}\n"
                f"reason: test content not in dict format."
            )
            continue

        # api in v2 format, convert to v3 testcase
        if "request" in test_content and "name" in test_content:#httprunner2版本用例转为3版本用例
            test_content = ensure_testcase_v3_api(test_content)

        if "config" not in test_content:#config为必须配置。
            logger.warning(
                f"Invalid testcase/testsuite file: {test_file}\n"
                f"reason: missing config part."
            )
            continue
        elif not isinstance(test_content["config"], Dict):#校验config配置,不是dict说明是错误的
            logger.warning(
                f"Invalid testcase/testsuite file: {test_file}\n"
                f"reason: config should be dict type, got {test_content['config']}"
            )
            continue

        # ensure path absolute
        test_content.setdefault("config", {})["path"] = test_file #设置一个path

        # testcase
        if "teststeps" in test_content:
            try:
                testcase_pytest_path = make_testcase(test_content)#maketestcase
                pytest_files_run_set.add(testcase_pytest_path) #将结果存入py文件集合。
            except exceptions.TestCaseFormatError as ex:
                logger.warning(
                    f"Invalid testcase file: {test_file}\n{type(ex).__name__}: {ex}"
                )
                continue

        # testsuite
        elif "testcases" in test_content: #在测试步骤中调用了其他文件,则调用make_testsuite
            try:
                make_testsuite(test_content)
            except exceptions.TestSuiteFormatError as ex:
                logger.warning(
                    f"Invalid testsuite file: {test_file}\n{type(ex).__name__}: {ex}"
                )
                continue

        # invalid format
        else:
            logger.warning(
                f"Invalid test file: {test_file}\n"
                f"reason: file content is neither testcase nor testsuite"
            )

__make函数内容实现的是传入目录或者文件列表,输出 pytest_files_run_set,它是py文件的集合。

""" save generated pytest files to run, except referenced testcase
"""
pytest_files_run_set: Set = set()

可以看到它是一个集合,保存产生的pytest文件,但是未包含那些引用的用例文件。

解析yaml/json文件时,如果有teststep,则执行如下两行制作py文件

testcase_pytest_path = make_testcase(test_content)#maketestcase
pytest_files_run_set.add(testcase_pytest_path) #将结果存入py文件集合。

如果有测试套件,则执行如下制作py文件(结果未存入pytest_files_run_set)

make_testsuite(test_content)

make_testcase

该函数用来制作测试用例。

def make_testcase(testcase: Dict, dir_path: Text = None):
    """convert valid testcase dict to pytest file path"""
    # ensure compatibility with testcase format v2
    testcase = ensure_testcase_v3(testcase)

    # validate testcase format
    load_testcase(testcase)#校验用例格式

    testcase_abs_path = __ensure_absolute(testcase["config"]["path"])#获取用例绝对路径
    logger.info(f"start to make testcase: {testcase_abs_path}")

    testcase_python_abs_path, testcase_cls_name = convert_testcase_path(
        testcase_abs_path
    )#获取pytest文件的路径
    if dir_path:
        testcase_python_abs_path = os.path.join(
            dir_path, os.path.basename(testcase_python_abs_path)
        )#输出pytest文件的路径

    global pytest_files_made_cache_mapping #全局变量
    if testcase_python_abs_path in pytest_files_made_cache_mapping:
        return testcase_python_abs_path #如果该pytest文件已经在缓存中,则直接返回路径

    config = testcase["config"]
    config["path"] = convert_relative_project_root_dir(testcase_python_abs_path)
    config["variables"] = convert_variables(
        config.get("variables", {}), testcase_abs_path
    )#config的变量

    # prepare reference testcase
    imports_list = []
    teststeps = testcase["teststeps"]#摘出每个步骤的teststep
    for teststep in teststeps:
        if not teststep.get("testcase"):#如果步骤中有testcase关键字,则会把它引用的文件给读出来。
            continue

        # make ref testcase pytest file
        ref_testcase_path = __ensure_absolute(teststep["testcase"])
        test_content = load_test_file(ref_testcase_path)

        if not isinstance(test_content, Dict):
            raise exceptions.TestCaseFormatError(f"Invalid teststep: {teststep}")

        # api in v2 format, convert to v3 testcase
        if "request" in test_content and "name" in test_content:
            test_content = ensure_testcase_v3_api(test_content)

        test_content.setdefault("config", {})["path"] = ref_testcase_path
        ref_testcase_python_abs_path = make_testcase(test_content)

        # override testcase export
        ref_testcase_export: List = test_content["config"].get("export", [])
        #如果有导出关键字,改为列表
        if ref_testcase_export:
            step_export: List = teststep.setdefault("export", [])
            step_export.extend(ref_testcase_export)
            teststep["export"] = list(set(step_export))

        # prepare ref testcase class name
        ref_testcase_cls_name = pytest_files_made_cache_mapping[
            ref_testcase_python_abs_path
        ]
        teststep["testcase"] = ref_testcase_cls_name

        # prepare import ref testcase
        ref_testcase_python_relative_path = convert_relative_project_root_dir(
            ref_testcase_python_abs_path
        )
        ref_module_name, _ = os.path.splitext(ref_testcase_python_relative_path)
        ref_module_name = ref_module_name.replace(os.sep, ".")
        import_expr = f"from {ref_module_name} import TestCase{ref_testcase_cls_name} as {ref_testcase_cls_name}"
        if import_expr not in imports_list:
            imports_list.append(import_expr)

    testcase_path = convert_relative_project_root_dir(testcase_abs_path)
    # current file compared to ProjectRootDir
    diff_levels = len(testcase_path.split(os.sep))#判断用例文件时第几级
 

    data = {
        "version": __version__,
        "testcase_path": testcase_path,
        "diff_levels": diff_levels,
        "class_name": f"TestCase{testcase_cls_name}",
        "imports_list": imports_list,
        "config_chain_style": make_config_chain_style(config),
        "parameters": config.get("parameters"),
        "teststeps_chain_style": [
            make_teststep_chain_style(step) for step in teststeps
        ],
    }
    content = __TEMPLATE__.render(data)#返回pytest文件内容 这里用的是jinja2的模板渲染数据

    # ensure new file's directory exists
    dir_path = os.path.dirname(testcase_python_abs_path)
    if not os.path.exists(dir_path):
        os.makedirs(dir_path)

    with open(testcase_python_abs_path, "w", encoding="utf-8") as f:
        f.write(content) #写入py文件
    pytest_files_made_cache_mapping[testcase_python_abs_path] = testcase_cls_name
    __ensure_testcase_module(testcase_python_abs_path) #确保pytest文件在模块里--给模块加一个init文件

    logger.info(f"generated testcase: {testcase_python_abs_path}")

    return testcase_python_abs_path

HttpRunner3.x 源码解析(3)-main_make生成用例文件_第3张图片

在函数内,需要对全局变量进行变更,或者重定义时,需要用global对变量宣言。

python中global的用法 (baidu.com) 

""" cache converted pytest files, avoid duplicate making
"""
pytest_files_made_cache_mapping: Dict[Text, Text] = {}

缓存pytest文件,避免重复制作

HttpRunner3.x 源码解析(3)-main_make生成用例文件_第4张图片 

 

 data = {
        "version": __version__,
        "testcase_path": testcase_path,
        "diff_levels": diff_levels,
        "class_name": f"TestCase{testcase_cls_name}",
        "imports_list": imports_list,
        "config_chain_style": make_config_chain_style(config),
        "parameters": config.get("parameters"),
        "teststeps_chain_style": [
            make_teststep_chain_style(step) for step in teststeps
        ],
    }
    content = __TEMPLATE__.render(data)#返回pytest文件内容

 

__TEMPLATE__ = jinja2.Template()是一个模板,传入data后,调用render函数,将data数据渲染进模板

(15条消息) Jinja2 模板用法_jinja2怎么设置查找全局的templates_格洛米爱学习的博客-CSDN博客 

 make_testsuite

用来将testsuite转换为pytest文件,testsuite是指有testcases关键字的测试文件。

这里还发现了源码的问题,就是testcases下面写的extract和validate不生效,而是直接用的testcase用例的extract和validate。


config:
    name: "查询用户信息"
    base_url: "https://api.pity.fun"

testcases:
-   name: 登录成功
    testcase: ./testcases/login.yml
    extract:
        token: body.data.token
    validate:
        -   eq: [ "status_code", 200 ]
        -   eq: [ body.code,0 ]
        -   eq: [ body.msg,"哈哈哈" ]
-
    name: 查询用户信息
    testcase: ./testcases/query_custom.yml
    validate:
        - eq: ["status_code", 200]
        - eq: [body.code,0]
        - eq: [body.msg,"aaaa"]

HttpRunner3.x 源码解析(3)-main_make生成用例文件_第5张图片 在make_testsuite源码

def make_testsuite(testsuite: Dict):
    """convert valid testsuite dict to pytest folder with testcases"""
    # validate testsuite format
    load_testsuite(testsuite)
    print("testsuite")
    print(testsuite)

    testsuite_config = testsuite["config"]
    testsuite_path = testsuite_config["path"]
    testsuite_variables = convert_variables(
        testsuite_config.get("variables", {}), testsuite_path
    )

    logger.info(f"start to make testsuite: {testsuite_path}")

    # create directory with testsuite file name, put its testcases under this directory
    testsuite_path = ensure_file_abs_path_valid(testsuite_path)
    testsuite_dir, file_suffix = os.path.splitext(testsuite_path)
    # demo_testsuite.yml => demo_testsuite_yml
    testsuite_dir = f"{testsuite_dir}_{file_suffix.lstrip('.')}"

    for testcase in testsuite["testcases"]:
        # get referenced testcase content
        testcase_file = testcase["testcase"]
        testcase_path = __ensure_absolute(testcase_file)
        testcase_dict = load_test_file(testcase_path)
        testcase_dict.setdefault("config", {})
        testcase_dict["config"]["path"] = testcase_path

        # override testcase name
        testcase_dict["config"]["name"] = testcase["name"]
        # override base_url
        base_url = testsuite_config.get("base_url") or testcase.get("base_url")
        if base_url:
            testcase_dict["config"]["base_url"] = base_url
        # override verify
        if "verify" in testsuite_config:
            testcase_dict["config"]["verify"] = testsuite_config["verify"]
        # override variables
        # testsuite testcase variables > testsuite config variables
        #testcase里的变量优先级>testsuite的config中的变量
        testcase_variables = convert_variables(
            testcase.get("variables", {}), testcase_path
        )
        testcase_variables = merge_variables(testcase_variables, testsuite_variables)
        # testsuite testcase variables > testcase config variables
        testcase_dict["config"]["variables"] = convert_variables(
            testcase_dict["config"].get("variables", {}), testcase_path
        )
        testcase_dict["config"]["variables"].update(testcase_variables)

        # override weight
        if "weight" in testcase:
            testcase_dict["config"]["weight"] = testcase["weight"]
        logger.info(f"testsuite_dir{testsuite_dir}")
        logger.info(f"testcase_dict{testcase_dict}")
        # 将testcase中的内容和testsuite的目录传给make_testcase,生成pytest文件
        testcase_pytest_path = make_testcase(testcase_dict, testsuite_dir)
        pytest_files_run_set.add(testcase_pytest_path)

 从源码中可以看到,并没有对testcase中的export和validate进行改写,所以testsuite中的export和validate没有被用到。

解决:

HttpRunner3.x 源码解析(3)-main_make生成用例文件_第6张图片

 

#如果testsuite中存在validate,则用testsuie中的 overright validate
        if "validate" in testcase:
            testcase_dict['teststeps'][0]['validate'] = testcase["validate"]

        #override export
        if "extract" in testcase:
            testcase_dict["teststeps"][0]["extract"] = testcase["extract"]

需要注意的是,testsuite里引用的testcase文件,必须只能有一个步骤。

 HttpRunner3.x 源码解析(3)-main_make生成用例文件_第7张图片

运行程序可以看到,这里导出和断言,用的是试套件下的内容,解决了问题。

 

小技巧 

ensure_path_sep(tests_path)#路径兼容
os.path.isabs(tests_path) 判断是否为一个绝对路径
os.getcwd() 获取当前目录
os.path.join(os.getcwd(), tests_path) 拼接路径
 os.path.isdir 判断是否为目录
os.path.isfile 判断是否为一个文件
os.path.splitext("D:\Project\demo\testcases\demo_testcase_ref.yml") 获取文件后缀
 os.path.dirname(testcase_python_abs_path) 获取绝对路径文件的目录
os.path.exists 判断目录是否存在
os.makedirs(dir_path) 创建目录
if not os.path.exists(dir_path):
    os.makedirs(dir_path)

文件写入

with open(testcase_python_abs_path, "w", encoding="utf-8") as f:
    f.write(content)

打印前加f,可以在字符串里引用变量

logger.info(f"generated testcase: {testcase_python_abs_path}")
os.sep

用于系统路径中的分隔符

Windows系统上,文件的路径分隔符是 '\'

Linux系统上,文件的路径分隔符是 '/'

苹果Mac OS系统中是 ':'

Python 为满足跨平台的要求,使用os.sep能够在不同系统上采用不同的分隔符

你可能感兴趣的:(#,python,开发语言)