zoukankan      html  css  js  c++  java
  • 《HttpRunner源码阅读》【未完-待更新】

    HttpRunner

    • 源码地址:https://github.com/httprunner/httprunner
    • 当前版本:3.1.4
    • 功能介绍:接口自动化测试(支持输入.har文件、支持输出json/yaml文件)、性能测试框架
    • 底层使用:request、jinja2、pytest、locust
    • 使用人员:测试开发人员、自动化测试人员
    • 阅读时间:2020/10/24~待填入
    • 阅读目的:
      1.使用该框架到工作中:自动化测试
      2.改造框架:让其能根据测试用例自动生成测试代码,降低编写自动化测试脚本成本

    httprunner/httprunner目录

    util.py 公共工具模块封装:【12个函数,1个类】
    
    init_sentry_sdk                 # sentry_sdk库,初始化错误日志监控
    set_os_environ                  # 增加系统环境变量 os.environ
    unset_os_environ                # 删除系统环境变量 os.environ
    get_os_environ                  # 查询系统环境变量的值 os.en
    lower_dict_keys                 # 将字段里的keys转换成小写
    print_info                      # 美化打印dict对象
    omit_long_data                  # 超出固定字符省略,str/bytes
    get_platform                    # 获取程序信息
    sort_dict_by_custom_order       # 根据一定的顺序排序dict,使用sorted来处理
    ExtendJSONEncoder               # ==TODO:暂时还不知道干嘛用的==
    merge_variables                 # 两个dict,第一的dict的k-v会替换第二个dict的k-v,触发了if则不替换
    is_support_multiprocessing      # 用Queue来处理多线程
    gen_cartesian_product           # 生成笛卡尔积List[Dict]
    
    cli.py 命令行模块:【6个函数】
    
    init_parser_run(subparsers)     # 对run命令初始化解析
    main_run(extra_args)            # run命令的主函数;会调用compat.py模块的ensure_cli_args()函数-》调用make.py模块的main_make()函数得到testcase_path_list-》调用pytest.main(extra_args_new)运行测试用例py脚本,设置了--tb=short打印较少错误输出信息
    main()                          # 主程序入口,对一级命令 [httprunner]和二级命令[startproject,run,har2case,make]的处理
    main_hrun_alias()               # hrun命令别名主函数
    main_make_alias()               # make命令别名主函数
    main_har2case_alias()           # har2case命令别名主函数
    
    make.py --TODO:未完成-- 将测试用例生成测试脚本模块;pytest_files_run_set常量是存储测试脚本的地方
    
    main_make(tests_paths)          # 生成测试用例脚本;会调用compat.py模块的ensure_path_sep()函数处理路径兼容性问题-》调用__make()方法
    __make(tests_path)              # 根据testcase/testsuite的绝对路径生成pytest-file并缓存到pytest_files_run_set;参数是文件夹就调用loader.py模块的load_folder_files()方法,得到files_list-》根据files_list遍历,是"_test.py"后缀的就pytest_files_run_set.add(test_file),遍历完就结束该方法-》否则就调用loader.py的load_test_file()得到test_content-》test_content里有"request"and"name"就调用ensure_testcase_v3_api()处理test_content自身-》"teststeps" in test_content就调用make_testcase()得到testcase_pytest_path就加入pytest_files_run_set然后结束;"testcases" in test_content就调用make_testsuite(test_content)
    make_testcase(testcase,...)     # **将有效的测试用例字典转换为pytest文件路径;看下面的详细解析,这里是核心**
    __ensure_absolute()             # 调用loader.py的load_project_meta()得到project_meta信息
    convert_testcase_path(testcase_abs_path)        # 将单个YAML/JSON测试用例路径转换为python文件; 调用ensure_file_abs_path_valid()得到testcase_new_path-》然后对testcase_new_path处理,会处理文件名称file_name,replace("_", "")
    __ensure_testcase_module(path)            # 确保pytest文件在python模块中,按需生成__init__.py
    
    loader.py --TODO:未完成--
    
    load_folder_files()             # 默认对文件夹递归解析文件夹,返回后缀为.yml/.yaml/.json/_test.py文件的list;调用os.walk()方法获取递归目录下所有文件-》调用str.endswith()方法判断字符串是否以指定后缀结尾;
    load_test_file()                # 加载testcase/testsuite文件(.yaml/.json)获得test_file_content;调用os.path.splitext()函数将文件名和扩展名分开-》根据扩展名调用不同的方法返回test_file_content:比如.yaml/.yml后缀的就调用_load_yaml_file()方法
    _load_yaml_file()               # 加载yaml文件并检查文件内容格式;使用with上下文管理open(yaml_file, mode="rb")-》调用yaml.load()加载获取的content,发生异常对其捕捉后,输出格式化日志再用人为抛异常raise
    load_testcase()                 # 验证testcase格式;使用pydantic包进行数据验证,返回testcase_obj;调用models.py模块的TestCase.parse_obj()方法来验证字段是否满足
    load_project_meta()             # 加载项目元数据;project_meta常量使用models.py的ProjectMeta
    convert_relative_project_root_dir()      # 根据project_meta.RootDir将绝对路径转换为相对路径; 调用load_project_meta()
    load_testsuite()                # 验证testsuite格式;处理方式和load_testcase()一样
    
    models.py --TODO:未完成--
    
    TestCase类                  # 继承pydantic.TestCase.BaseModel;类属性config使用TConfig类结构
    TConfig类                   # 继承BaseModel
    ProjectMeta类               # 继承BaseModel
    
    compat.py --TODO:未完成-- 兼容性问题处理模块:v2和v3之间的测试用例格式、其他兼容性问题【12个函数】
    
    ensure_path_sep()               # 处理文件路径的Windows和linux的兼容性问题;使用内置包os.sep.join方法,根据所在的系统,自动使用分割符
    ensure_cli_args()               # 兼容v2的cli args
    ensure_testcase_v3_api()        # convert api in v2 to testcase format v3 兼容v2,将v2的testcase转换成v3的格式
    ensure_testcase_v3()            # ensure compatibility with testcase format v2
    convert_variables()             # 转换变量:Union[Dict, List, Text]->Dict[Text, Any]
    
    ext/har2case/__init__.py har2case包初始化:【2个函数】
    
    init_har2case_parser(subparsers)            # har2case命令行参数设置;return parser
    main_har2case(args)                         # har2case命令主函数入口;会调用./core.py模块的gen_testcase(output_file_type)函数
    
    ext/har2case/core.py har2case测试用例文件生成模块:将.har文件转成各种不同的文件格式【HarParser类12个方法,1个函数】
    
    ensure_file_path(path)                  # 判断path的合法性:不能为空、后缀是.har、;会调用httprunner/compat.py/ensure_path_sep()函数
    HarParser.__init__(...)                 # 初始化方法;初始化参数har_file_path、filter_str、exclude_str-》 会调用ensure_file_path()函数处理har_file_path
    HarParser.gen_testcase(file_type)       # HarParser类给外部生成用例的方法,会生成测试用例文件;会调用self._make_testcase()方法-》得到的返回信息传给./utils.py/dump_json-》得到测试用例文件
    HarParser._make_testcase()              # 从.har文件里提取测试用例需要的信息;会调用self._prepare_config()方法、self._prepare_teststeps()方法-》得到testcase
    HarParser._prepare_config()             # 测试用例的config配置块
    HarParser._prepare_teststeps()          # 测试用例的teststeps列表;会调用./utils.py/load_har_log_entries(self.har_file_path)-》遍历返回数据,根据命令行输入的--filter和--exclude参数值过滤url-》调用_prepare_teststep()方法,将返回信息放进的teststeps列表
    HarParser._prepare_teststep(entry_json) # 将输入的entry_json进行解析得到teststep信息;会调用__make_request_url()...将获取的数据存进teststep_dict
    
    ext/har2case/utils.py --TODO:未完成-- har2case工具模块:【6个函数】
    
    dump_yaml()                             # 将.har文件序列化成.yaml格式文件;使用第三方包的yaml.dump()
    dump_json()                             # 将.har文件序列化成.json格式文件;使用第三方包的json.dumps()
    load_har_log_entries(file_path)         # 解析.har文件,过滤后只留下log.entries的value
    

    make.py-make_testcase()解析

    将有效的测试用例字典转换为pytest文件路径

        # 调用compat.py的ensure_testcase_v3()处理v2兼容性
        testcase = ensure_testcase_v3(testcase)
    
        # 调用loader.py模块的load_testcase()验证testcase format
        load_testcase(testcase)
    
        #  调用__ensure_absolute()验证testcase["config"]["path"]
        testcase_abs_path = __ensure_absolute(testcase["config"]["path"])
        
        # 将单个YAML/JSON测试用例路径转换为python文件
        testcase_python_abs_path, testcase_cls_name = convert_testcase_path(
            testcase_abs_path
        )
        
        #  ---处理testcase_python_abs_path---start
        if dir_path:
            testcase_python_abs_path = os.path.join(
                dir_path, os.path.basename(testcase_python_abs_path)
            )
    
        global pytest_files_made_cache_mapping
        if testcase_python_abs_path in pytest_files_made_cache_mapping:
            return testcase_python_abs_path
        #  ---处理testcase_python_abs_path---end
        
        # 得到用例的config数据;
        config = testcase["config"]
        # 增加config的path: 调用loader.py的convert_relative_project_root_dir()
        config["path"] = convert_relative_project_root_dir(testcase_python_abs_path)
        # 处理variables:调用compat.py的convert_variables()方法
        config["variables"] = convert_variables(
            config.get("variables", {}), testcase_abs_path
        )
    
        # ---测试用例---start
        imports_list = []
        teststeps = testcase["teststeps"]
        for teststep in teststeps:
            # teststep里没有testcase字段就遍历下一个teststep;testcase是teststep里引用其他用例
            if not teststep.get("testcase"):
                continue
    
            # 调用__ensure_absolute,获取“引用测试用例”的路径
            ref_testcase_path = __ensure_absolute(teststep["testcase"])
            # 获取“引用测试用例”的pytest文件;调用loader.py的test_content()方法
            test_content = load_test_file(ref_testcase_path)
    
            if not isinstance(test_content, Dict):
                raise exceptions.TestCaseFormatError(f"Invalid teststep: {teststep}")
    
            # api为v2格式,转换为v3测试用例
            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
            # 调用自己,获得测试用例的pytest文件路径
            ref_testcase_python_abs_path = make_testcase(test_content)
    
            # 覆盖"引用测试用例"的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))
    
            # 设置teststep["testcase"],value为class name
            ref_testcase_cls_name = pytest_files_made_cache_mapping[
                ref_testcase_python_abs_path
            ]
            teststep["testcase"] = ref_testcase_cls_name
    
            # 导入“引用测试用例”的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)
          
        # ---测试用例---end
    
      
        testcase_path = convert_relative_project_root_dir(testcase_abs_path)
        # 当前文件与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
            ],
        }
        
        # __TEMPLATE__是py testcase的模板;__TEMPLATE__使用的是jinja2.Template()方法
        content = __TEMPLATE__.render(data)
    
        # 确保新文件的目录是存在的
        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_files_made_cache_mapping[testcase_python_abs_path] = testcase_cls_name
        
        # 确保是pytest测试用例在一个package包里(所在的目录里有__init__.py文件)
        __ensure_testcase_module(testcase_python_abs_path)
    
        logger.info(f"generated testcase: {testcase_python_abs_path}")
    
        return testcase_python_abs_path
    

    make.py-make_testsuite()解析

    使用测试用例将有效的testsuite dict转换为pytest文件夹

        # 验证 testsuite 格式
        load_testsuite(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}")
    
        # 用testsuite文件名创建目录,将其测试用例放在该目录下
        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_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"]
    
            # make testcase
            testcase_pytest_path = make_testcase(testcase_dict, testsuite_dir)
            pytest_files_run_set.add(testcase_pytest_path)
    
    Hole yor life get everything if you never give up.
  • 相关阅读:
    采用get方式提交数据到服务器实例
    android之HttpURLConnection
    Android中的传感器
    有符号类型无符号类型转换
    一些常用位运算示例
    C++ / CLI 调用 C++ /Native 随记
    Linux Shell Demo
    Linux Shell 脚本入门
    Linux 编译 websocket++
    Linux 编写c++程序之openssl
  • 原文地址:https://www.cnblogs.com/1fengchen1/p/13899952.html
Copyright © 2011-2022 走看看