原文:httprunnerV3源码——hrun命令详解 - 简书 (jianshu.com)
httprunner命令介绍
在安装httprunner库之后,就可以使用httprunner
命令了。
- httprunner命令:
在命令行工具输入httprunner -h
,从输出可以看到,httprunner提供run
、startproject
、har2case
、make
四个子命令:
run
命令用于运行测试用例
startproject
命令用于创建httprunner脚手架项目
har2case
命令用于将har文件转成httprunner使用的yml/json文件
make
命令用于将yml/json用于转成pytest用例
> httprunner -h
usage: httprunner [-h] [-V] {run,startproject,har2case,make} ...
One-stop solution for HTTP(S) testing.
positional arguments:
{run,startproject,har2case,make}
sub-command help
run Make HttpRunner testcases and run with pytest.
startproject Create a new project with template structure.
har2case Convert HAR(HTTP Archive) to YAML/JSON testcases for
HttpRunner.
make Convert YAML/JSON testcases to pytest cases.
optional arguments:
-h, --help show this help message and exit
-V, --version show version
- 其它命令
httprunner还提供了三个子命令的缩写,httprunner run
可使用hrun
命令代替,同样的,hmake
和har2case
分别是httprunner make
和httprunner har2case
的缩写。另外,还提供了locusts
命令用于执行压测。
在pyproject.toml
文件中定义了httprunner的命令并指定了入口:
# pyproject.toml
[tool.poetry.scripts]
httprunner = "httprunner.cli:main"
hrun = "httprunner.cli:main_hrun_alias"
hmake = "httprunner.cli:main_make_alias"
har2case = "httprunner.cli:main_har2case_alias"
locusts = "httprunner.ext.locust:main_locusts"
命令执行过程
httprunner命令的入口在cli
模块的main()
函数,在main()
函数中解析了run
、startproject
、har2case
、make
命令参数,最终分发到具体的执行函数处理。
如果用户输入不是这些命令也不是-V/--version
、-h/--help
命令,则退出。
# cli.py
def main():
...
if sys.argv[1] == "run":
# 执行httprunner测试
sys.exit(main_run(extra_args))
elif sys.argv[1] == "startproject":
# 创建httprunner脚手架项目
main_scaffold(args)
elif sys.argv[1] == "har2case":
# 通过har生成httprunner测试用例
main_har2case(args)
elif sys.argv[1] == "make":
# 通过httprunner测试用例生成pytest测试用例
main_make(args.testcase_path)
run命令
run命令由cli.py
的main_run
函数处理,处理流程如下:
- 进一步处理用户输入,适配httprunnerV2.x参数
- 通过路径参数获取测试文件,转成pytest用例
- 将生成的pytest用例文件路径和处理过的用户输入参数传入pytest执行
# cli.py
def main_run(extra_args) -> enum.IntEnum:
capture_message("start to run")
# 适配V2.x命令参数
extra_args = ensure_cli_args(extra_args)
# 进一步处理参数,区分文件路径参数和非文件路径参数,不存在文件路径参数则结束执行
...
# 生成pytest测试用例文件,生成的文件不存在则结束执行
testcase_path_list = main_make(tests_path_list)
if not testcase_path_list:
sys.exit(1)
# 添加--tb=short参数
...
# 执行pytest测试
extra_args_new.extend(testcase_path_list)
return pytest.main(extra_args_new)
- 关于对v2.x的适配,请查看htttprunnerV3源码——V2.x适配
生成pytest用例
main_run
函数中,处理用户入参后,调用make.py
的main_make
函数将hrun用例文件转换为pytest用例文件。
# make.py
def main_make(tests_paths: List[Text]) -> List[Text]:
# 参数为空则返回空数组
...
for tests_path in tests_paths:
# 确保与 Linux 和 Windows 的不同路径分隔符兼容,相对路径转绝对路径
...
try:
# 生成pytest用例文件
__make(tests_path)
except exceptions.MyBaseError as ex:
logger.error(ex)
sys.exit(1)
# 格式化pytest用例文件
pytest_files_format_list = pytest_files_made_cache_mapping.keys()
format_pytest_with_black(*pytest_files_format_list)
# 返回pytest用例文件路径数组
return list(pytest_files_run_set)
hrun用例转pytest用例
获取hrun用例文件路径
tests_path
在main_make
函数中已经全部处理成了绝对路径,但路径可能是用例文件也可能是用例目录,__make
函数首先把传入的路径数组转换成用例文件路径数组。
# make.py
def __make(tests_path: Text) -> NoReturn:
test_files = []
if os.path.isdir(tests_path):
# 目录路径,将目录及子目录下的所有用例文件取出
files_list = load_folder_files(tests_path)
test_files.extend(files_list)
elif os.path.isfile(tests_path):
# 文件路径,直接添加
test_files.append(tests_path)
else:
raise exceptions.TestcaseNotFound(f"Invalid tests path: {tests_path}")
...
-
load_folder_files
函数
位于loader.py
,该函数返回指定目录及其子目录
下的所有以.yml
、.yaml
、.json
、_test.py
结尾的文件路径
通过hrun用例生成pytest用例
经过上一步操作,得到了仅包含测试用例文件路径的数组test_files
,遍历数组,为每个hrun用例生成pytest用例:
# make.py
def __make(tests_path: Text) -> NoReturn:
...
for test_file in test_files:
# _test.py结尾已经是pytest用例,无需处理,直接添加到待执行集合
...
# 加载测试用例内容,如果内容不是Dict类型,结束本次处理,不执行该用例(此处省略异常捕获语句)
test_content = load_test_file(test_file)
...
# V2.x中的api格式转换为V3的testcase格式
if "request" in test_content and "name" in test_content:
test_content = ensure_testcase_v3_api(test_content)
# 用例缺少配置(config属性)或配置不是Dict类型,结束本次处理,不执行该用例
...
# 设置path为当前文件绝对路径配置
test_content.setdefault("config", {})["path"] = test_file
if "teststeps" in test_content:
# 文件内容是testcase,生成pytest用例文件,将pytest用例添加到待执行集合(此处省略异常捕获语句)
testcase_pytest_path = make_testcase(test_content)
pytest_files_run_set.add(testcase_pytest_path)
elif "testcases" in test_content:
# 文件内容是testsuite,通过其中的testcase生成pytest用例文件,并添加到待执行集合(此处省略异常捕获语句)
make_testsuite(test_content)
...
-
pytest_files_run_set
集合
make.py
中定义的Set类型变量,用于保存生成的pytest文件以运行,引用的testcase除外 -
load_test_file
函数
位于loader.py
,该函数返回指定文件的内容。指定文件不存在或不以.json/.yml/.yaml
结尾则抛出异常 -
ensure_testcase_v3_api
函数
位于compat.py
,将V2.x中的api格式内容转换为V3统一的testcase格式,返回一个包含config和teststeps属性的字典数据 -
make_testcase
函数
位于make.py
,通过testcase对象生成pytest用例文件,返回文件路径 -
make_testsuite
函数
位于make.py
,遍历testsuite中的testcases,通过testcase对象生成pytest用例文件,并将pytest文件路径添加到pytest_files_run_set集合
从上述代码段可知,hrun用例转pytest用例的核心方法是make_testcase
和make_testsuite
。
make_testcase
make_testcase函数中,首先校验和格式化用例内容,确保测试用例内容是httprunnerV3的格式
# make.py
def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
# V2.x用例格式转V3格式
testcase = ensure_testcase_v3(testcase)
# 校验内容格式,load_testcase接收Dict类型入参,返回一个TestCase对象
load_testcase(testcase)
# 获取用例文件绝对路径
testcase_abs_path = __ensure_absolute(testcase["config"]["path"])
...
在得到确定的V3格式用例内容后,开始转换pytest格式用例。
首先需要确定生成的pytest用例文件路径、文件名和类名:
def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
...
# 获取pytest文件路径和类名
testcase_python_abs_path, testcase_cls_name = convert_testcase_path(testcase_abs_path)
if dir_path:
# 指定pytest文件目录
testcase_python_abs_path = os.path.join(dir_path, os.path.basename(testcase_python_abs_path))
convert_testcase_path
函数根据原始的yaml/json
文件路径和文件名确定将要生成的pytest文件名和类名:如果原始文件名以数字开头,就在文件名前加T
;原始文件名中的.
、-
、替换为_
;文件名以_test.py
结尾,最终生成一个蛇形命名的文件名;而类名则是将蛇形的文件名字符串(不包含_test.py)转换为大驼峰格式字符串。
例如:
原始文件名 | pytest文件名 | 类名 |
---|---|---|
2021-user.login.yml | T2021_user_login_test.py | T2021UserLogin |
request-with-variables.json | request_with_variables_test.py | RequestWithVariables |
确定pytest文件路径后,在全局的pytest文件缓存池pytest_files_made_cache_mapping
查找文件是否已经生成,已生成就直接返回文件路径。在执行多个用例时,用例之间可能存在引用关系,把已生成的pytest文件记录到全局变量中可以防止重复生成文件。
如果pytest文件未生成,接下来就开始转换用例内容,将httprunner的用例格式转换为pytest用例格式
config部分
def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
...
config = testcase["config"]
# pytest文件相对于测试项目根目录的路径
config["path"] = convert_relative_project_root_dir(testcase_python_abs_path)
# 校验变量格式,并处理$变量引用
config["variables"] = convert_variables(config.get("variables", {}), testcase_abs_path)
convert_variables
返回字典型变量集合,函数定义如下:
- def convert_variables(raw_variables: Union[Dict, List, Text], test_path: Text):
如果raw_variables是字典类型,如{"var1": 1}
,无需处理直接返回。
如果raw_variables是字典数组类型,如[{"var1": 1}, {"var2": 2}]
,则将所有元素合并到同一个字典后返回。
如果raw_variables是文本类型,且存在引用并返回解析后数据,否则直接返回原始文本。
raw_variables不是上述3种类型则抛出异常。
所以,httprunner用例的变量可以有3种写法,如果是写文本,可以写成"${sum_two(1, 2)}"
、$foo1
、HttpRunner/${get_httprunner_version()}
、"value1=$aaa&value2=$bbb"
等多种形式。
teststeps部分
用例配置解析完成后,开始封装pytest用例数据。
- 如果测试步骤引用了其它用例,先处理引用的用例文件:
1、加载用例内容,并校验内容格式,格式错误抛出异常
2、V2.x适配,将V2.x的api格式转换为V3的testcase格式
3、在配置中增加path属性,值为引用用例文件的绝对路径
4、处理完用例内容后,递归调用make_testcase,生成引用用例的pytest文件
5、将引用用例的export列表转换成测试步骤的export列表
6、通过文件路径从全局的pytest文件池获取引用用例pytest文件的类名,结果如:RequestWithFunctions
7、通过类名和文件路径生成对用例pytest文件的import语句,结果如:from . import TestCaseRequestWithFunctions as RequestWithFunctions
封装pytest用例数据,生成pytest用例文件:
# 获取当前原始用例(yml、josn文件)相对于测试根目录(执行测试命令的)的路径
testcase_path = convert_relative_project_root_dir(testcase_abs_path)
# 计算当前用例相对于执行测试根目录的深度
diff_levels = len(testcase_path.split(os.sep))
data = {
# httprunner版本号
"version": __version__,
"testcase_path": testcase_path,
"diff_levels": diff_levels,
# 最终生成的类名加上TestCase前缀,如:TestCaseRequestWithFunctions
"class_name": f"TestCase{testcase_cls_name}",
# 对其它用例的依赖
"imports_list": imports_list,
# 用例配置代码格式化,如:Config("xxx").variables(xx=xxx, ...).verify(...).export(...)...
"config_chain_style": make_config_chain_style(config),
# 参数化配置
"parameters": config.get("parameters"),
# 测试步骤代码格式化,如:RunRequest("xxx").variables(xx=xxx, ...)...
"teststeps_chain_style": [make_teststep_chain_style(step) for step in teststeps],
}
# 通过jinja2模板,生成pytest用例内容
content = __TEMPLATE__.render(data)
# 生成python文件并写入文件内容
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文件缓存池,key=python文件路径,value=用例类名
pytest_files_made_cache_mapping[testcase_python_abs_path] = testcase_cls_name
# 确保pytest文件目录一定存在__init__.py文件,有这个文件,文件目录才会被识别成一个python模块
__ensure_testcase_module(testcase_python_abs_path)
# 生成pytest文件结束,返回文件绝对路径
return testcase_python_abs_path
make_testsuite
作者:卫青臣
链接:https://www.jianshu.com/p/8eb29bd630e7
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
网友评论