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.
  • 相关阅读:
    Hdu 5396 Expression (区间Dp)
    Lightoj 1174
    codeforces 570 D. Tree Requests (dfs)
    codeforces 570 E. Pig and Palindromes (DP)
    Hdu 5385 The path
    Hdu 5384 Danganronpa (AC自动机模板)
    Hdu 5372 Segment Game (树状数组)
    Hdu 5379 Mahjong tree (dfs + 组合数)
    Hdu 5371 Hotaru's problem (manacher+枚举)
    Face The Right Way---hdu3276(开关问题)
  • 原文地址:https://www.cnblogs.com/1fengchen1/p/13899952.html
Copyright © 2011-2022 走看看