美文网首页Httprunner学习测试小记python
htttprunnerV3源码——V2.x适配

htttprunnerV3源码——V2.x适配

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

    原文:htttprunnerV3源码——V2.x适配 - 简书 (jianshu.com)

    httprunner升级到v3版本后,用例格式和v2.x版本不一样了。在v3版本中,作者推荐大家回归到直接使用码编写用例,而不是写yml/Json文件,并且在v3版本中去除了测试用例分层的api层,统一为testcase。

    但是v3版本依然保留了对yml/json用例的支持,并且对v2.x的用例也做了一定的适配,本篇文章主要内容就是httprunner v3版本源码中对v2.x的适配。

    v2.x命令参数适配

    由于底层测试框架从之前的unittest更换为了pytest,所以v3版本的启动命令参数和之前的不同了,为此,v3首先对启动命令做了低版本适配。

    httprunner run/hrun命令的入口在cli.pymain_run函数,该函数首先对命令参数进行了低版本适配:

    # cli.py
    def main_run(extra_args) -> enum.IntEnum:
        capture_message("start to run")
        # hrun v2.x命令参数的适配
        extra_args = ensure_cli_args(extra_args)
        ...
    
    

    ensure_cli_argscompat.py中的函数,对httprunnerV2.x中的--failfast--report-file--save-tests命令参数进行了适配:

    # compat.py
    def ensure_cli_args(args: List) -> List:  
        """ ensure compatibility with deprecated cli args in v2"""
        if "--failfast" in args:  
            # 删除--failfast参数,不再处理
            args.pop(args.index("--failfast"))
    
        if "--report-file" in args:  
            # --report-file参数改为--html --self-contained-html  
            index = args.index("--report-file")
            args[index] = "--html"
            args.append("--self-contained-html")
    
        if "--save-tests" in args:  
            # 生成测试报告摘要
            args.pop(args.index("--save-tests"))
            _generate_conftest_for_summary(args)  
    
        return args
    
    

    前两个很好理解,--failfast参数在v3版本中被弃用了;--report-file参数换成了pytest框架支持的参数--html --self-contained-html;而对--save-tests参数的适配则做了较多处理。

    --save-tests参数

    在v2.x版本中,执行测试指定--save-tests参数,即可将运行过程中的中间数据保存为日志文件,日志文件保存在测试项目根目录的 logs 文件夹,生成的文件有如下三个(XXX为测试用例名称):

    • XXX.loaded.json:测试用例加载后的数据结构内容,加载包括测试用例文件(YAML/JSON)、debugtalk.py、.env 等所有项目文件
    • XXX.parsed.json:测试用例解析后的数据结构内容,解析内容包括测试用例引用(API/testcase)、变量计算和替换、base_url 拼接等
    • XXX.summary.json:测试报告生成前的数据结构内容

    在v3版本中,默认会将运行过程数据以{UUID}.run.log文件形式保存到logs目录下,如果指定--save-tests参数,则会在logs目录下生成all.summary.json(测试路径是用例目录)或XXX.summary.json(测试路径是单个用例文件)。

    生成conftest.py

    _generate_conftest_for_summary函数中,通过在测试目录下生成pytest的conftest.py文件来生成summary.json文件

    # compat.py
    def _generate_conftest_for_summary(args: List):
        # 从args获取一个路径参数赋值给test_path变量,没有路径参数则结束执行
        for arg in args:
            if os.path.exists(arg):
                test_path = arg
                # FIXME: several test paths maybe specified  ->  多个路径参数只取第一个
                break
        else:
            sys.exit(1)
        conftest_content = '''此处省略conftest.py文件内容...'''
        # 通过test_path得出测试项目根目录、conftest.py文件路径(根目录下)、logs目录路径(根目录下)
        ...
        if os.path.isdir(test_path):  
            # 如果测试路径是目录,在logs目录下生成all.summary.json  
            file_folder_path = os.path.join(logs_dir_path, test_path_relative_path)  
            dump_file_name = "all.summary.json"  
        else:  
            # 测试路径是文件,在父目录下生成 {文件名}.summary.json  
            file_relative_folder_path, test_file = os.path.split(test_path_relative_path)  
            file_folder_path = os.path.join(logs_dir_path, file_relative_folder_path)  
            test_file_name, _ = os.path.splitext(test_file)  
            dump_file_name = f"{test_file_name}.summary.json"
        summary_path = os.path.join(file_folder_path, dump_file_name)  
        # 将报告路径传入conftest.py的session_fixture
        conftest_content = conftest_content.replace("{{SUMMARY_PATH_PLACEHOLDER}}", summary_path)
        # 生成conftest.py文件,写入文件内容conftest_content
        ...
    
    

    在上述代码中,根据传入的测试路径,可以得出以下路径:

    测试路径 项目根目录 logs目录 conftest.py路径 summary.json路径
    D:\test\demo.yml D:\test\ D:\test\logs\ D:\test\conftest.py D:\test\logs\demo.summary.json
    D:\testsuite\ D:\testsuite\ D:\testsuite\logs\ D:\testsuite\conftest.py D:\testsuite\logs\all.summary.json

    summary.json文件由conftest.py创建,pytest运行时,会自动识别项目根目录下的conftest.py文件。

    上述代码生成的conftest.py文件内容如下:

    session_fixture函数设置了@pytest.fixture(scope="session", autouse=True),表示在执行测试前后自动运行一次。

    conftest中fixtrue的执行时机通过yield关键字区分,在yield之前的代码会在执行测试前运行,在yield之后的代码会在测试完成后运行。

    以下代码在执行测试前记录开始时间,测试完成后生成指定的summary.json文件。

    # 此处省略import
    ...
    @pytest.fixture(scope="session", autouse=True)
    def session_fixture(request):
        """setup and teardown each task"""
        logger.info(f"start running testcases ...")
        start_at = time.time()
    
        yield
    
        logger.info(f"task finished, generate task summary for --save-tests")
        summary = {
            "success": True,
            "stat": {
                "testcases": {"total": 0, "success": 0, "fail": 0},
                "teststeps": {"total": 0, "failures": 0, "successes": 0},
            },
            "time": {"start_at": start_at, "duration": time.time() - start_at},
            "platform": get_platform(),
            "details": [],
        }
        for item in request.node.items:
            testcase_summary = item.instance.get_summary()
            summary["success"] &= testcase_summary.success
    
            summary["stat"]["testcases"]["total"] += 1
            summary["stat"]["teststeps"]["total"] += len(testcase_summary.step_datas)
            if testcase_summary.success:
                summary["stat"]["testcases"]["success"] += 1
                summary["stat"]["teststeps"]["successes"] += len(
                    testcase_summary.step_datas
                )
            else:
                summary["stat"]["testcases"]["fail"] += 1
                summary["stat"]["teststeps"]["successes"] += (
                    len(testcase_summary.step_datas) - 1
                )
                summary["stat"]["teststeps"]["failures"] += 1
    
            testcase_summary_json = testcase_summary.dict()
            testcase_summary_json["records"] = testcase_summary_json.pop("step_datas")
            summary["details"].append(testcase_summary_json)
    
        summary_path = "E:\Projects\Python\httprunner\examples\postman_echo\logs\request_methods\hardcode.summary.json"
        summary_dir = os.path.dirname(summary_path)
        os.makedirs(summary_dir, exist_ok=True)
    
        with open(summary_path, "w", encoding="utf-8") as f:
            json.dump(summary, f, indent=4, ensure_ascii=False, cls=ExtendJSONEncoder)
    
        logger.info(f"generated task summary: {summary_path}")
    
    

    v2.x用例格式转v3格式

    httprunner执行测试需要将httprunner用例转换为pytest用例,在这之前会将v2.x的用例内容转换为v3的内容格式。

    make.py是主要负责生成pytest用例的模块,核心函数是make_testcase,该函数负责将httprunner用例转换为pytest用例。

    # make.py
    def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
        """convert valid testcase dict to pytest file path"""
        # V2.x用例格式转V3格式
        testcase = ensure_testcase_v3(testcase)
        ...
        teststeps = testcase["teststeps"]
        for teststep in teststeps:
            ...
            # V2.x的api格式转换为V3的testcase格式
            if "request" in test_content and "name" in test_content:
                test_content = ensure_testcase_v3_api(test_content)
            ...
        ...
    
    

    在make_testcase函数中,首先确保用例内容符合v3版本的格式:

    # compat.py
    def ensure_testcase_v3(test_content: Dict) -> Dict:
        v3_content = {"config": test_content["config"], "teststeps": []}
    
        # 如果用例中不存在测试步骤或测试步骤不是数组类型,则结束测试
        if "teststeps" not in test_content:
            sys.exit(1)
        if not isinstance(test_content["teststeps"], list):
            sys.exit(1)
    
        for step in test_content["teststeps"]:  
            teststep = {}  
            if "request" in step:
                # 将step的request对象属性重新排序
                teststep["request"] = _sort_request_by_custom_order(step.pop("request"))  
            elif "api" in step:
                # V2.x的api字段名换成testcase,V3不再使用测试用例分层
                teststep["testcase"] = step.pop("api")  
            elif "testcase" in step:  
                teststep["testcase"] = step.pop("testcase")  
            else:
                raise exceptions.TestCaseFormatError(f"Invalid teststep: {step}")  
            # 将step的name、variables、setup_hooks、extract等属性更新到teststep
            teststep.update(_ensure_step_attachment(step))  
            # teststep对象属性重新排序
            teststep = _sort_step_by_custom_order(teststep) 
            v3_content["teststeps"].append(teststep)  
        return v3_content
    
    

    内容格式处理完后,还要将v2.x的api层转换为testcase:

    # compat.py
    def ensure_testcase_v3_api(api_content: Dict) -> Dict:
        logger.info("convert api in v2 to testcase format v3")
    
        teststep = {
            # request属性字段重新排序,内容不变
            "request": _sort_request_by_custom_order(api_content["request"]),
        }
        teststep.update(_ensure_step_attachment(api_content))
    
        teststep = _sort_step_by_custom_order(teststep)
    
        config = {"name": api_content["name"]}
        extract_variable_names: List = list(teststep.get("extract", {}).keys())
        if extract_variable_names:
            config["export"] = extract_variable_names
    
        return {
            "config": config,
            "teststeps": [teststep],
        }
    
    

    相关文章

      网友评论

        本文标题:htttprunnerV3源码——V2.x适配

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