美文网首页Httprunner学习测试小记python
httprunnerV3源码——hrun命令详解

httprunnerV3源码——hrun命令详解

作者: 祁小彬 | 来源:发表于2021-11-13 03:03 被阅读0次

    原文:httprunnerV3源码——hrun命令详解 - 简书 (jianshu.com)

    httprunner命令介绍

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

    • httprunner命令:
      在命令行工具输入httprunner -h,从输出可以看到,httprunner提供runstartprojecthar2casemake四个子命令:
      run命令用于运行测试用例
      startproject命令用于创建httprunner脚手架项目
      har2case命令用于将har文件转成httprunner使用的yml/json文件
      make命令用于将yml/json用于转成pytest用例
    > 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
    
    
    • 其它命令
      httprunner还提供了三个子命令的缩写,httprunner run可使用hrun命令代替,同样的,hmakehar2case分别是httprunner makehttprunner har2case的缩写。另外,还提供了locusts命令用于执行压测。

    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}")
        ...
    
    
    • load_folder_files函数
      位于loader.py,该函数返回指定目录及其子目录下的所有以.yml.yaml.json_test.py结尾的文件路径

    通过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)
            ...
    
    
    • pytest_files_run_set集合
      make.py中定义的Set类型变量,用于保存生成的pytest文件以运行,引用的testcase除外
    • load_test_file函数
      位于loader.py,该函数返回指定文件的内容。指定文件不存在或不以.json/.yml/.yaml结尾则抛出异常
    • ensure_testcase_v3_api函数
      位于compat.py,将V2.x中的api格式内容转换为V3统一的testcase格式,返回一个包含config和teststeps属性的字典数据
    • make_testcase函数
      位于make.py,通过testcase对象生成pytest用例文件,返回文件路径
    • make_testsuite函数
      位于make.py,遍历testsuite中的testcases,通过testcase对象生成pytest用例文件,并将pytest文件路径添加到pytest_files_run_set集合

    从上述代码段可知,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返回字典型变量集合,函数定义如下:

    • def convert_variables(raw_variables: Union[Dict, List, Text], test_path: Text):
      如果raw_variables是字典类型,如{"var1": 1},无需处理直接返回。
      如果raw_variables是字典数组类型,如[{"var1": 1}, {"var2": 2}],则将所有元素合并到同一个字典后返回。
      如果raw_variables是文本类型,且存在,则解析引用并返回解析后数据,否则直接返回原始文本。
      raw_variables不是上述3种类型则抛出异常。
      所以,httprunner用例的变量可以有3种写法,如果是写文本,可以写成"${sum_two(1, 2)}"$foo1HttpRunner/${get_httprunner_version()}"value1=$aaa&value2=$bbb"等多种形式。
    teststeps部分

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

    • 如果测试步骤引用了其它用例,先处理引用的用例文件:
      1、加载用例内容,并校验内容格式,格式错误抛出异常
      2、V2.x适配,将V2.x的api格式转换为V3的testcase格式
      3、在配置中增加path属性,值为引用用例文件的绝对路径
      4、处理完用例内容后,递归调用make_testcase,生成引用用例的pytest文件
      5、将引用用例的export列表转换成测试步骤的export列表
      6、通过文件路径从全局的pytest文件池获取引用用例pytest文件的类名,结果如:RequestWithFunctions
      7、通过类名和文件路径生成对用例pytest文件的import语句,结果如:from . import TestCaseRequestWithFunctions as RequestWithFunctions

    封装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

    作者:卫青臣
    链接:https://www.jianshu.com/p/8eb29bd630e7
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    相关文章

      网友评论

        本文标题:httprunnerV3源码——hrun命令详解

        本文链接:https://www.haomeiwen.com/subject/bjeezltx.html