httprunnerV3源码——hrun命令详解

2021-09-08  本文已影响0人  卫青臣

httprunner命令介绍

在安装httprunner库之后,就可以使用httprunner命令了。

> httprunner -h
usage: httprunner [-h] [-V] {run,startproject,har2case,make} ...

One-stop solution for HTTP(S) testing.

positional arguments:
  {run,startproject,har2case,make}
                        sub-command help
    run                 Make HttpRunner testcases and run with pytest.
    startproject        Create a new project with template structure.
    har2case            Convert HAR(HTTP Archive) to YAML/JSON testcases for
                        HttpRunner.
    make                Convert YAML/JSON testcases to pytest cases.

optional arguments:
  -h, --help            show this help message and exit
  -V, --version         show version

pyproject.toml文件中定义了httprunner的命令并指定了入口:

# pyproject.toml
[tool.poetry.scripts]  
httprunner = "httprunner.cli:main"  
hrun = "httprunner.cli:main_hrun_alias"  
hmake = "httprunner.cli:main_make_alias"  
har2case = "httprunner.cli:main_har2case_alias"  
locusts = "httprunner.ext.locust:main_locusts"

命令执行过程

httprunner命令的入口在cli模块的main()函数,在main()函数中解析了runstartprojecthar2casemake命令参数,最终分发到具体的执行函数处理。
如果用户输入不是这些命令也不是-V/--version-h/--help命令,则退出。

# cli.py
def main():
    ...
    if sys.argv[1] == "run":  
        # 执行httprunner测试
        sys.exit(main_run(extra_args))  
    elif sys.argv[1] == "startproject":  
        # 创建httprunner脚手架项目
        main_scaffold(args)  
    elif sys.argv[1] == "har2case":  
        # 通过har生成httprunner测试用例
        main_har2case(args)  
    elif sys.argv[1] == "make":  
        # 通过httprunner测试用例生成pytest测试用例
        main_make(args.testcase_path)

run命令

run命令由cli.pymain_run函数处理,处理流程如下:

  1. 进一步处理用户输入,适配httprunnerV2.x参数
  2. 通过路径参数获取测试文件,转成pytest用例
  3. 将生成的pytest用例文件路径和处理过的用户输入参数传入pytest执行
# cli.py
def main_run(extra_args) -> enum.IntEnum:
    capture_message("start to run")
    # 适配V2.x命令参数
    extra_args = ensure_cli_args(extra_args)
    # 进一步处理参数,区分文件路径参数和非文件路径参数,不存在文件路径参数则结束执行
    ...
    
    # 生成pytest测试用例文件,生成的文件不存在则结束执行
    testcase_path_list = main_make(tests_path_list)
    if not testcase_path_list:
        sys.exit(1)
        
    # 添加--tb=short参数
    ...
    # 执行pytest测试
    extra_args_new.extend(testcase_path_list)  
    return pytest.main(extra_args_new)

生成pytest用例

main_run函数中,处理用户入参后,调用make.pymain_make函数将hrun用例文件转换为pytest用例文件。

# make.py
def main_make(tests_paths: List[Text]) -> List[Text]:
    # 参数为空则返回空数组
    ...
    for tests_path in tests_paths:
        # 确保与 Linux 和 Windows 的不同路径分隔符兼容,相对路径转绝对路径
        ...
        try:
            # 生成pytest用例文件
            __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)
    
    # 返回pytest用例文件路径数组
    return list(pytest_files_run_set)

hrun用例转pytest用例

获取hrun用例文件路径

tests_pathmain_make函数中已经全部处理成了绝对路径,但路径可能是用例文件也可能是用例目录,__make函数首先把传入的路径数组转换成用例文件路径数组。

# make.py
def __make(tests_path: Text) -> NoReturn:
    test_files = []  
    if os.path.isdir(tests_path): 
        # 目录路径,将目录及子目录下的所有用例文件取出
        files_list = load_folder_files(tests_path)  
        test_files.extend(files_list)  
    elif os.path.isfile(tests_path): 
        # 文件路径,直接添加
        test_files.append(tests_path)  
    else:  
        raise exceptions.TestcaseNotFound(f"Invalid tests path: {tests_path}")
    ...

通过hrun用例生成pytest用例

经过上一步操作,得到了仅包含测试用例文件路径的数组test_files,遍历数组,为每个hrun用例生成pytest用例:

# make.py
def __make(tests_path: Text) -> NoReturn:
    ...
    for test_file in test_files:
        # _test.py结尾已经是pytest用例,无需处理,直接添加到待执行集合
        ...
        # 加载测试用例内容,如果内容不是Dict类型,结束本次处理,不执行该用例(此处省略异常捕获语句)
        test_content = load_test_file(test_file)
        ...
        # V2.x中的api格式转换为V3的testcase格式
        if "request" in test_content and "name" in test_content:
            test_content = ensure_testcase_v3_api(test_content)

        # 用例缺少配置(config属性)或配置不是Dict类型,结束本次处理,不执行该用例
        ...
        # 设置path为当前文件绝对路径配置
        test_content.setdefault("config", {})["path"] = test_file

        if "teststeps" in test_content:
            # 文件内容是testcase,生成pytest用例文件,将pytest用例添加到待执行集合(此处省略异常捕获语句)
            testcase_pytest_path = make_testcase(test_content)
            pytest_files_run_set.add(testcase_pytest_path)
        elif "testcases" in test_content:
            # 文件内容是testsuite,通过其中的testcase生成pytest用例文件,并添加到待执行集合(此处省略异常捕获语句)
            make_testsuite(test_content)
        ...

从上述代码段可知,hrun用例转pytest用例的核心方法是make_testcasemake_testsuite

make_testcase

make_testcase函数中,首先校验和格式化用例内容,确保测试用例内容是httprunnerV3的格式

# make.py
def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
    # V2.x用例格式转V3格式
    testcase = ensure_testcase_v3(testcase) 
    # 校验内容格式,load_testcase接收Dict类型入参,返回一个TestCase对象
    load_testcase(testcase)
    # 获取用例文件绝对路径
    testcase_abs_path = __ensure_absolute(testcase["config"]["path"])
    ...

在得到确定的V3格式用例内容后,开始转换pytest格式用例。
首先需要确定生成的pytest用例文件路径、文件名和类名:

def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
    ...
    # 获取pytest文件路径和类名
    testcase_python_abs_path, testcase_cls_name = convert_testcase_path(testcase_abs_path)  
    if dir_path:
        # 指定pytest文件目录
        testcase_python_abs_path = os.path.join(dir_path, os.path.basename(testcase_python_abs_path))

convert_testcase_path函数根据原始的yaml/json文件路径和文件名确定将要生成的pytest文件名和类名:如果原始文件名以数字开头,就在文件名前加T;原始文件名中的.-替换为_;文件名以_test.py结尾,最终生成一个蛇形命名的文件名;而类名则是将蛇形的文件名字符串(不包含_test.py)转换为大驼峰格式字符串。
例如:

原始文件名 pytest文件名 类名
2021-user.login.yml T2021_user_login_test.py T2021UserLogin
request-with-variables.json request_with_variables_test.py RequestWithVariables

确定pytest文件路径后,在全局的pytest文件缓存池pytest_files_made_cache_mapping查找文件是否已经生成,已生成就直接返回文件路径。在执行多个用例时,用例之间可能存在引用关系,把已生成的pytest文件记录到全局变量中可以防止重复生成文件。

如果pytest文件未生成,接下来就开始转换用例内容,将httprunner的用例格式转换为pytest用例格式

config部分
def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
    ...
    config = testcase["config"]
    # pytest文件相对于测试项目根目录的路径  
    config["path"] = convert_relative_project_root_dir(testcase_python_abs_path)  
    # 校验变量格式,并处理$变量引用 
    config["variables"] = convert_variables(config.get("variables", {}), testcase_abs_path)

convert_variables返回字典型变量集合,函数定义如下:

teststeps部分

用例配置解析完成后,开始封装pytest用例数据。

封装pytest用例数据,生成pytest用例文件:

# 获取当前原始用例(yml、josn文件)相对于测试根目录(执行测试命令的)的路径
testcase_path = convert_relative_project_root_dir(testcase_abs_path)
# 计算当前用例相对于执行测试根目录的深度
diff_levels = len(testcase_path.split(os.sep))

data = {
    # httprunner版本号
    "version": __version__,
    "testcase_path": testcase_path,
    "diff_levels": diff_levels,
    # 最终生成的类名加上TestCase前缀,如:TestCaseRequestWithFunctions
    "class_name": f"TestCase{testcase_cls_name}",
    # 对其它用例的依赖
    "imports_list": imports_list,
    # 用例配置代码格式化,如:Config("xxx").variables(xx=xxx, ...).verify(...).export(...)...
    "config_chain_style": make_config_chain_style(config),
    # 参数化配置
    "parameters": config.get("parameters"),
    # 测试步骤代码格式化,如:RunRequest("xxx").variables(xx=xxx, ...)...
    "teststeps_chain_style": [make_teststep_chain_style(step) for step in teststeps],
}
# 通过jinja2模板,生成pytest用例内容
content = __TEMPLATE__.render(data)

# 生成python文件并写入文件内容
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)
# 已生成文件添加到全局pytest文件缓存池,key=python文件路径,value=用例类名
pytest_files_made_cache_mapping[testcase_python_abs_path] = testcase_cls_name
# 确保pytest文件目录一定存在__init__.py文件,有这个文件,文件目录才会被识别成一个python模块
__ensure_testcase_module(testcase_python_abs_path)

# 生成pytest文件结束,返回文件绝对路径
return testcase_python_abs_path
make_testsuite

格式化用例文件

未完待续

startproject命令

未完待续

har2case命令

未完待续

make命令

未完待续

上一篇下一篇

猜你喜欢

热点阅读