httprunnerV3源码——hrun命令详解

httprunner命令介绍

在安装httprunner库之后,就可以使用httprunner命令了。

  • httprunner命令:
    在命令行工具输入httprunner -h,从输出可以看到,httprunner提供runstartprojecthar2casemake四个子命令:
    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命令代替,同样的,hmakehar2case分别是httprunner makehttprunner 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()函数中解析了runstartprojecthar2casemake命令参数,最终分发到具体的执行函数处理。
如果用户输入不是这些命令也不是-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.pymain_run函数处理,处理流程如下:

  1. 进一步处理用户输入,适配httprunnerV2.x参数
  2. 通过路径参数获取测试文件,转成pytest用例
  3. 将生成的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)

生成pytest用例

main_run函数中,处理用户入参后,调用make.pymain_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_pathmain_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_testcasemake_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)}"$foo1HttpRunner/${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

格式化用例文件

未完待续

startproject命令

未完待续

har2case命令

未完待续

make命令

未完待续

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容