Jason Pan

httprunner

潘忠显 / 2020-11-22


使用文档阅读

官方文档地址:https://docs.httprunner.org

两种配置方式

使用插件的方式调用debugtalk

约定优于配置convention over configuration)[1],也称作按约定编程[2],是一种软件设计范式,旨在减少软件开发人员需做决定的数量,获得简单的好处,而又不失灵活性。

Based on the philosophy of Convention over configuration, each project should and could only have one debugtalk.py file. This file has multiple functions.

debugtalk.py

标示项目根节点,所有testcase的相对路径都是依赖于这个path

自定义函数放在这里,testcase用到的所有函数都在这个文件里

尽量避免使用同名变量,如果实在需要,需要了解变量的优先级

In a testcase, variables priority are in the following order:

In a testsuite, variables priority are in the following order:

scaffold 脚手架 创建项目时候自带的一些内容,自带一些测试

hrun demo

jmespath 提取和校验返回的json

allure 美化、强化测试报告

locust 性能测试

CLI command supported, perfect combination with CI/CD.

安装中的注意事项

Python环境要求,验证安装

https://zhuanlan.zhihu.com/p/218287637

Key Features

Check Installation 一节中提到,如果安装HttpRunner成功了,会有5个命令可以使用。

When HttpRunner is installed, 5 commands will be added in your system.

其实除了locusts之外,主要设计run、make、har2case三个模块。

知道Python的requests库

用什么工具获得HAR file

Charles Proxy

Each testcase is a subclass of HttpRunner, and must have two class attributes: config and teststeps.

代码阅读

github: https://github.com/httprunner/httprunner

项目七月之前很活跃,近4个月没有commit

作者博客:https://debugtalk.com/ 关于httprunner的文章 https://debugtalk.com/tags/HttpRunner/

一、文件目录结构

为了更清晰的说明,省略了各个目录下的__init__.py文件。

httprunner
├── __main__.py
├── app
│   ├── main.py
│   └── routers
│       ├── debug.py
│       ├── debugtalk.py
│       └── deps.py
├── builtin
│   ├── comparators.py
│   └── functions.py
├── cli.py
├── client.py
├── compat.py                # 处理V2和V3之间的兼容性
├── exceptions.py
├── ext
│   ├── har2case             # har2case的源码位置
│   │   ├── core.py          # HarParser类实现
│   │   └── utils.py
│   ├── locust
│   │   └── locustfile.py
│   └── uploader
│       └── __init__.py
├── loader.py
├── make.py
├── models.py                # 基本参数的数据结构定义(类)
├── parser.py                # utils的补充,主要与参数解析相关的通用函数(字符串、正则等扩展处理、执行hook等)
├── response.py
├── runner.py
├── scaffold.py
├── testcase.py
└── utils.py                 # 通用工具函数:系统相关信息获取、信息打印、dict的扩展处理等

二、整体流程

framework

配置解析相关

以.har文件解析为例,HarParser类中实现har的解析和转换,其gen_testcase主要以下两步:

  1. 将配置和测试步骤解析到特定结构的testcase中,_prepare_config读取配置,_prepare_teststeps读取测试步骤,拼装testcase字典。

  2. 使用utils中的函数,将testcase导出成json、yaml、pytest。

testcase就是有两个key(config和teststeps)的dict。json和yaml的配置解析之后,可以直接得到这种结构,而无需额外操作。通过这个testcase做后续的make等动作。

代码生成

根据配置解析成testcase之后,会做一些V2和V3兼容性处理,会对相对路径、绝对路径做一些路径转换(大部分使用绝对路径处理)。 再将参数处理成**jinja2.Template模版引擎需要的参数,进行render(渲染/参数替换),生成对应的pytest文件**

pytest模板的内容

生成继承HttpRunner的testcase类,生成main函数入口。大部分功能的实现都是在基类中,比如:

项目本身V2和V3的兼容性

由compat.py中函数做处理,暂时不关心。

三、代码结构

正如文档所说,所有的testcase都是继承自HttpRunner的类,拥有两个属性configteststeps。因此针对Config、Step以及HttpRunner三部分做梳理,最后理解一个TestCase的状态流转(Step转移)的情况。

class HttpRunner(object):
    config: Config
    teststeps: List[Step]

1. Config

定义了config的属性,成员函数分为三类:

TConfig结构是实际在HttpRunner中使用的类型。

2. Step

class Step是对几个步骤的抽象,类似于状态转移。

个人理解class RunRequest也是一种Step,也可以作为被封装在一起。

RunRequest && RunTestCase

RunRequest is used in a step to make request to API and do some extraction or validations for response.

RunTestCase is used in a step to reference another testcase call.

RunRequestRunTestCase 是上述详细步骤的封装,用户只需要知道这两种Case,而不需要知道中间Step的状态转移

3. HttpRunner

run_path()
  load_testcase_file()
  run_testcase()
    遍历teststeps():
      合并变量
      __run_step()
        __run_step_request/__run_step_testcase
      更新提取变量
      追加测试统计数据

__run_step_request

__run_step_request

4. 状态转移

通过三个例子来说明上边Step之间的状态转移

a. basic.yml: RunRequest("get with params in url")

配置: ./examples/httpbin/basic.yml

代码: ./examples/httpbin/basic_test.py

Step(
    RunRequest("get with params in url")
    .get("/get?a=1&b=2")
    .validate()
    .assert_equal("status_code", 200)
    .assert_equal("body.args", {"a": "1", "b": "2"})
)

testcase 1

b. request_with_testcase_reference.yml: RunRequest("get with params")

配置: examples/postman_echo/request_methods/request_with_testcase_reference.yml

代码: examples/postman_echo/request_methods/request_with_testcase_reference_test.py

Step(
    RunRequest("get with params")
    .with_variables(
        **{"foo1": "bar11", "foo2": "bar21", "sum_v": "${sum_two(1, 2)}"}
    )
    .get("/get")
    .with_params(**{"foo1": "$foo1", "foo2": "$foo2", "sum_v": "$sum_v"})
    .with_headers(**{"User-Agent": "HttpRunner/${get_httprunner_version()}"})
    .extract()
    .with_jmespath("body.args.foo2", "foo3")
    .validate()
    .assert_equal("status_code", 200)
    .assert_equal("body.args.foo1", "bar11")
    .assert_equal("body.args.sum_v", "3")
    .assert_equal("body.args.foo2", "bar21")
)

testcase 2

c. request_with_testcase_reference.yml: RunTestCase("request with functions")

配置: examples/postman_echo/request_methods/request_with_testcase_reference.yml

代码: examples/postman_echo/request_methods/request_with_testcase_reference_test.py

Step(
    RunTestCase("request with functions")
    .with_variables(
        **{"foo1": "testcase_ref_bar1", "expect_foo1": "testcase_ref_bar1"}
    )
    .setup_hook("${sleep(0.1)}")
    .call(RequestWithFunctions)
    .teardown_hook("${sleep(0.2)}")
    .export(*["foo3"])
)

testcase 3

5. 几种机制的说明

ChainCall机制

支持使用链式调用的类型,是通过函数体执行函数本身的操作(比如设置内容)+执行完返回self(自身对象的引用)来实现链式调用的。

比如configer类中:

def variables(self, **variables) -> "Config":
    self.__variables.update(variables)
    return self

TODO hook机制

setup_hooks和teardown_hooks的区别

render

_maketestcase

从功能和使用文档分析该项目中包含的模块

Python相关

解参数

python中 **dict的含义

def foo(a, b, c):
    print(a, b, c)

obj = {'b':10, 'c':'lee'}

foo(100,**obj)

typing — 类型标注支持

Python 运行时并不强制标注函数和变量类型。类型标注可被用于第三方工具,比如类型检查器、集成开发环境、静态检查器等。

from typing import Dict, Text, Union, Callable

在函数 greeting 中,参数 name 预期是 str 类型,并且返回 str 类型。: str是强制的类型校验,-> str是起注释的作用,并没真正的约束能力。

def greeting(name: str) -> str:
    return 'Hello ' + name
greeting(1)

上边的程序会报错:

Traceback (most recent call last):
  File "t.py", line 5, in <module>
    greeting(1)
  File "t.py", line 2, in greeting
    return 'Hello ' + name
TypeError: must be str, not int

可以简单的用C++中的类型限定去理解。

dict和typing.Dict的区别是:后者是dict的泛型版本,对其中的类型可以不做特别的限定。

def count_words(text: str) -> Dict[str, int]:

Dict对标注返回类型比较有用。如果要标注参数的话,使用如 Mapping 的抽象容器类型是更好的选择。

不过httprunner中依然使用Dict来定义类型VariablesMapping:

VariablesMapping = Dict[Text, Any]
__session_variables: VariablesMapping = {}
variables: Union[VariablesMapping, Text] = {}
method: MethodEnum = MethodEnum.GET

models中大量使用了上边方式的去定义类的"属性"

pydantic

Data validation and settings management using python type annotations.

pydantic enforces type hints at runtime, and provides user friendly errors when data is invalid.

Define how data should be in pure, canonical python; validate it with pydantic.

使用"python type annotations"来校验不受信任的数据是否符合我们要求的格式,并将值设置到对应的结构中。

stack

        caller_frame = inspect.stack()[1]
        self.__path = caller_frame.filename

术语

SUT (System under test) – 被测系统