htttprunnerV3源码——V2.x适配

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适配)