httprunner源码学习(四)ensure_testcase_v3_api()、make_testcase()
ensure_testcase_v3_api()
__make()方法的第40行调用此方法;
上篇39行说了,如果传入的 json/yaml 文件,第一层(最外层的键)包含 "request" 和 "name",那么就执行这个方法。
作者的意思很明显,如果传入的json/yaml 文件 只包含一个接口,就执行这个方法。
那如果有多个接口呢,当然会有其他的处理逻辑,但是应该相差不大,理解了这个,其他的不难。
这里我们要有一个概念,下面这个方法接收的参数,大概是这个样子:
{
"name": "headers",
"request": {
"url": "/headers",
"method": "GET"
},
"validate": [
{
"eq": ["status_code", 200]
},
{
"eq": ["body.headers.Host", "httpbin.org"]
}
]
}
再来看源码
# httprunner/compat.py :: ensure_testcase_v3_api
def ensure_testcase_v3_api(api_content: Dict) -> Dict:
logger.info("convert api in v2 to testcase format v3")
teststep = { # 定义一个测试步骤,调用自定义方法,将传入的接口信息,根据他定义好的顺序来排序
"request": _sort_request_by_custom_order(api_content["request"]),
}
teststep.update(_ensure_step_attachment(api_content)) # 调用自定义方法,作用是:将api_content中的键值对经过处理后添加到teststep中,这里主要处理了 extract 和 validate 这两个键值(处理比较复杂,后面再研究吧),其他的原封不动直接添加
teststep = _sort_step_by_custom_order(teststep) # 调用自定义方法,将 teststep 根据他定义好的顺序来排序,实现逻辑跟上面 _sort_request_by_custom_order 这个方法类似
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, # config的键有:name、export
"teststeps": [teststep], # teststeps就是将传入的api_content里面的某些字段处理了一下(排序、提取等),然后放一个列表里面
}
make_testcase()
__make()方法的第61行调用了此方法。
这里我们要知道调用make_testcase方法时传递的参数大概是个什么样子:
{
"teststeps": [
{
"name": "",
"request": {},
"validate": []
}
],
"config": {}
}
接下来看源码,说实话这个make_testcase方法有点复杂,行数也多,还用到了递归,有点超出我的能力范围了,尽力看吧。
# httprunner/make.py :: make_testcase
def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
"""convert valid testcase dict to pytest file path"""
# ensure compatibility with testcase format v2
testcase = ensure_testcase_v3(testcase) # 数据处理,目的是为了乡下兼容V2版本
# validate testcase format
load_testcase(testcase) # 数据类型校验
testcase_abs_path = __ensure_absolute(testcase["config"]["path"]) # 路径处理
logger.info(f"start to make testcase: {testcase_abs_path}")
testcase_python_abs_path, testcase_cls_name = convert_testcase_path(
testcase_abs_path
)
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
config = testcase["config"]
config["path"] = convert_relative_project_root_dir(testcase_python_abs_path)
config["variables"] = convert_variables(
config.get("variables", {}), testcase_abs_path
)
# prepare reference testcase
imports_list = []
teststeps = testcase["teststeps"]
for teststep in teststeps:
if not teststep.get("testcase"):
continue
# make ref testcase pytest file
ref_testcase_path = __ensure_absolute(teststep["testcase"])
test_content = load_test_file(ref_testcase_path)
if not isinstance(test_content, Dict):
raise exceptions.TestCaseFormatError(f"Invalid teststep: {teststep}")
# api in v2 format, convert to v3 testcase
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
ref_testcase_python_abs_path = make_testcase(test_content)
# override testcase 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))
# prepare ref testcase class name
ref_testcase_cls_name = pytest_files_made_cache_mapping[
ref_testcase_python_abs_path
]
teststep["testcase"] = ref_testcase_cls_name
# prepare import ref 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)
testcase_path = convert_relative_project_root_dir(testcase_abs_path)
# current file compared to 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
],
}
content = __TEMPLATE__.render(data)
# ensure new file's directory exists
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
__ensure_testcase_module(testcase_python_abs_path)
logger.info(f"generated testcase: {testcase_python_abs_path}")
return testcase_python_abs_path

浙公网安备 33010602011771号