HttpRunner HttpRunner 的结果校验器优化

debugtalk · 2017年12月13日 · 最后由 loneyao 回复于 2020年09月04日 · 3234 次阅读

在测试用例中,包含预期结果这么一项,用于辅助测试人员执行测试用例时判断系统的功能是否正常。而在自动化测试中,我们的目标是让测试用例自动执行,因此自动化测试用例中同样需要包含预期结果一项,只不过系统响应结果不再由人工来进行判断,而是交由测试工具或框架来实现。

这部分功能对应的就是测试结果校验器(validator),基本上能称得上自动化测试工具或框架的都包含该功能特性。

设计之初

HttpRunner在设计之初,结果校验器(validator)的实现比较简单。

对于每一个test,可以指定 0 个或多个校验项,放置在validate中。在自动化测试执行的时候,会在发起 HTTP 请求、解析结果响应之后,逐个检查各个校验项,若存在任意校验项不通过的情况,则该test将终止并被标记为失败。

- test:
    name: get token
    request:
        url: http://127.0.0.1:5000/api/get-token
        method: GET
    extract:
        - token: content.token
    validate:
        - {"check": "status_code", "comparator": "eq", "expect": 200}
        - {"check": "content.token", "comparator": "len_eq", "expect": 16}

如上例所示,每一个校验项均为一个json结构,里面包含checkexpectcomparator三个属性字段。其中,check对应着要检查的字段,expect对应着检查字段预期的值,这两项是必须指定的;comparator字段对应着比较方法,若不指定,则默认采用eq,即检查字段与预期值相等。

为了实现尽可能强大的检查功能,check属性值可通过链式操作精确指定具体的字段,comparator也内置实现了大量的检查功能。

举个例子可能会更清晰些。假如某结构的响应结果如下:

// status code: 200

// response headers
{
   "Content-Type": "application/json"
}

// response body content
{
   "success": False,
   "person": {
       "name": {
           "first_name": "Leo",
           "last_name": "Lee",
       },
       "age": 29,
       "cities": ["Guangzhou", "Shenzhen"]
   }
}

那么假如我们要检查status codecheck就可以指定为status_code;假如要检查response headers中的Content-Typecheck就可以指定为headers.content-type;假如要检查response body中的first_namecheck就可以指定为content.person.name.first_name。可以看出,假如下一层级为字典结构,那么就可以通过.运算符指定下一层级的key,依次类推。

对于字段内容为列表list的情况略有不同,我们需要通过序号来指定具体检查哪一项内容。例如,Guangzhou对应的检查项为content.person.cities.0Shenzhen对应的检查项为content.person.cities.1

在比较方式(comparator)方面,HttpRunner除了eq,还内置了大量的检查方法。例如,我们可以通过gtgeltle等比较数值大小,通过len_eqlen_gtlen_lt等比较长度是否相等(列表、字典、字符串均适用),通过containscontained_by来判断包含关系,通过startswithendswith判断字符串的开头结尾,甚至通过regex_match来判断是否满足正则匹配等。详细的比较方式还有许多,需要时可查看comparator表格。

存在的局限性

在大多数情况下,HttpRunner的结果校验器(validator)是够用的。不过问题在于,框架不可能为用户实现所有的检查方法,假如用户需要某些特殊的检查方法时,HttpRunner就没法实现了。

这的确是一个问题,之前Junho2010提的 issue #29中举了一个例子,应该也算是比较有代表性。

发送请求时的数据使用了随机生成,然后需要比较结果中的数据是否是和这个相关(通过某个算法转换)。比如我输入的是 321,我的结果是(3+2+1) * avg(3+2+1)这种转化,目前的 comparator 是比较难于实现的。

要解决这个问题,最好的方式应该是在HttpRunner中实现自定义结果校验器的机制;用户在有需要的时候,可以自己编写校验函数,然后在validate中引用校验函数。之前也介绍过HttpRunner的热加载机制,《约定大于配置:ApiTestEngine 实现热加载机制》,自定义结果校验器应该也是可以采用这种方式来实现的。

第二个需要优化的点,HttpRunner的结果校验器还不支持变量引用,会造成某些场景下的局限性。例如,testwangchao曾提过一个 issue #52

接口 response 内,会返回数据库内的自增 ID。ID 校验的时候,希望expected为参数化的值。

validate:
    - {"check": "content.data.table_list.0.id", "expected": "$id"}

另外,在《ApiTestEngine,不再局限于 API 的测试》一文中有介绍过,结果提取器(extract)新增实现了通过正则表达式对任意文本响应内容的字段提取。考虑到结果校验器(validate)也需要先从结果响应中提取出特定字段才能与预期值进行比较,在具体实现上完全可以复用同一部分代码,因此在validatecheck部分也可以进行统一化处理。

经过前面的局限性问题描述,我们的改造目标也明确了,主要有三个方面:

  • 新增支持自定义结果校验器
  • 结果校验器中实现变量引用
  • 结果校验内容新增支持正则表达式提取

改造结果

具体的改造过程就不写了,有兴趣的同学可以直接阅读源码,重点查看httprunner/context.py中的parse_validatordo_validationvalidate三个函数。

经过优化后,改造目标中的三项功能都实现了。为了更好地展现改造后的结果校验器,此处将结合实例进行演示。

新增支持自定义结果校验器

先来看第一个优化项,新增支持自定义结果校验器。

假设我们需要使用 HTTP 响应状态码各个数字的和来进行校验,例如,201状态码对应的数字和为 3,503状态码对应的数字和为 8。该实例只是为了演示用,实际上并不会用到这样的校验方式。

首先,该种校验方式在HttpRunner中并没有内置,因此需要我们自己来实现。实现方式与热加载机制相同,只需要将自定义的校验函数放置到当前YAML/JSON文件同级或者父级目录的debugtalk.py中。

对于自定义的校验函数,需要遵循三个规则:

  • 自定义校验函数需放置到debugtalk.py
  • 参数有两个:第一个为原始数据,第二个为原始数据经过运算后得到的预期结果值
  • 在校验函数中通过assert将实际运算结果与预期结果值进行比较

对于前面提到的演示案例,我们就可以在debugtalk.py中编写如下校验函数。

def sum_status_code(status_code, expect_sum):
    """ sum status code digits
        e.g. 400 => 4, 201 => 3
    """
    sum_value = 0
    for digit in str(status_code):
        sum_value += int(digit)

    assert sum_value == expect_sum

然后,在YAML/JSON格式测试用例的validate中,我们就可以将校验函数名称sum_status_code作为comparator进行使用了。

- test:
    name: get token
    request:
        url: http://127.0.0.1:5000/api/get-token
        method: GET
    validate:
        - {"check": "status_code", "comparator": "eq", "expect": 200}
        - {"check": "status_code", "comparator": "sum_status_code", "expect": 2}

由此可见,自定义的校验函数sum_status_codeHttpRunner内置的校验方法eq在使用方式上完全相同,应该没有理解上的难度。

结果校验器中实现变量引用

对于第二个优化项,结果校验器中实现变量引用。在使用方式上我们应该与request中的变量引用一致,即通过$var的方式来引用变量var

- test:
    name: get token
    request:
        url: http://127.0.0.1:5000/api/get-token
        method: GET
    variables:
        - expect_status_code: 200
        - token_len: 16
    extract:
        - token: content.token
    validate:
        - {"check": "status_code", "comparator": "eq", "expect": "$expect_status_code"}
        - {"check": "content.token", "comparator": "len_eq", "expect": "$token_len"}
        - {"check": "$token", "comparator": "len_eq", "expect": "$token_len"}

通过以上示例可以看出,在结果校验器validate中,checkexpect均可实现实现变量的引用;而引用的变量,可以来自四种类型:

  • 当前test中定义的variables,例如expect_status_code
  • 当前test中提取(extract)的结果变量,例如token
  • 当前测试用例集testset中,先前test中提取(extract)的结果变量
  • 当前测试用例集testset中,全局配置config中定义的变量

check字段除了可以引用变量,以及保留了之前的链式操作定位字段(例如上例中的content.token)外,还新增了采用正则表达式提取内容的方式,也就是第三个优化项。

结果校验内容新增支持正则表达式提取

假设如下接口的响应结果内容为LB123abcRB789,那么要提取出abc部分进行校验,就可以采用如下描述方式。

- test:
    name: get token
    request:
        url: http://127.0.0.1:5000/api/get-token
        method: GET
    validate:
        - {"check": "LB123(.*)RB789", "comparator": "eq", "expect": "abc"}

可见在使用方式上与在结果提取器(extract)中完全相同。

结果校验器的进一步简化

最后,为了进一步简化结果校验的描述,我在validate中新增实现了一种描述方式。

简化后的描述方式与原始方式对比如下:

validate:
    - comparator_name: [check_item, expect_value]
    - {"check": check_item, "comparator": comparator_name, "expect": expect_value}

同样是前面的例子,采用新的描述方式后会更加简洁。而两种方式表达的含义是完全等价的。

- test:
    name: get token
    request:
        url: http://127.0.0.1:5000/api/get-token
        method: GET
    validate:
        - eq: ["status_code", $expect_status_code]
        - sum_status_code: ["status_code", 2]
        - len_eq: ["$token", $token_len]
        - len_eq: ["content.token", 16]
        - eq: ["LB123(.*)RB789", "abc"]

当然,此次优化保证了与历史版本的兼容,之前编写的测试用例脚本的运行是完全不会受到任何影响的。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 22 条回复 时间 点赞

结果校验器中实现变量引用,validate 中的 expect 中 不能调用 debugtalk 中的方法,可以优化一下

如果我在校验器里加入函数(从数据库里面取值 ${lift_check_attributes_goto_db('$sn')})怎么处理?下面的代码,函数 lift_check_attributes_goto_db('$sn') 取不到 MYSQL 数据库的值

  • test: name: 校验上传 api: api/vvvvv.yml variables:
    • sn: ${lift_get_radom_time_string()}
    • value: ${lift_check_attributes_goto_db('$sn')} validate:
    • {"check": "status_code", "comparator": "eq", "expect": 200}
    • {"check": "content.success", "comparator": "eq", "expect": 0}
    • {"check": "$value", "comparator": "eq", "expect": "$sn"}

报错如下:
AssertionError: validate: $value equals gongyoulift2019040817423338(str) ==> fail
${lift_check_attributes_goto_db('gongyoulift2019040817423338')}(str) equals gongyoulift2019040817423338(str)

请问一下,我要测的接口的 content 下有很多值,也不在同一个层级下,httprunner 可以支持将 content 下的全部字段都转化成 case 里的结果校验吗?

在结果校验中,有考虑支持输出一些信息么,例如 [check_item, expect_value, msg]

🙈 大神,弱弱问下,校验的内容是中文有编码问题,会报错 “Traceback (most recent call last):
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-3: ordinal not in range(128)” ,这个要怎么解决呀?
Python 2.7.9
HttpRunner 1.5.13
Windows

@debugtalk 作者大大,如果校验一个接口返回结果里不存在某个字段,比如接口返回是{
"photoId": "3xfv72ni24j4nzm",
"count": 20
} 想判断一下这个返回里是不存在 living 这个字段的,可以直接写- eq:["content.living",null] 这么写吗?感谢感谢~

@mickey2017 @shuqing2017 尴尬了,的确是支持的😅

meiyo 回复

谢谢提醒,我这边尝试了一下,也是 ok 的!

debugtalk 回复

我尝试了一下,支持了呀。

validate:
            - eq: ["content.result.success", True]
            - eq: ["content.data.-1.operatorName", $name]
meiyo 回复

没有支持哈


你好,对于列表,可以倒序引用吗?类似alist=[1,2,3]alist[-1]输出 3

yjq 回复

支持的,直接使用 $response 就可以引用了。
http://cn.httprunner.org/advanced/request-hook/

@debugtalk 大大,在 validate 部分,不能使用 $response 作为参数么?想自己实现函数做 json schema 校验,但是不知道怎么把 response 传到函数中。

HttpRunner 的测试用例分层机制 (已过期) 中提及了此贴 05月31日 18:21

今天在检测测试用例时候,突然想到个函数调用的问题。记得之前看到函数调用的方式是 ${fun_name},去函数调用那章复看理我一下的确如此,为什么结果检验这块就直接是

  • fun_name: xx xx 了,怕自己疏忽写错又回来翻了一遍,果然结果校验优化器这块可以这么写。😄

我有一个疑问,数据调用已经发生变化,怎么能重复使用进行回归测试呢

ChelseaZhang 回复

更新了

comparator 表格的链接无法打开了

6楼 已删除
5楼 已删除
yyy 回复

不限,只要是 HTTP 协议的都支持,翻下我之前写的文章吧

@debugtalk 楼主,现在是只支持格式为 json 的请求报文吗?

尹全旺 回复

测试报告会采用 allure 重新实现下,现在的测试报告太简陋了

仅楼主可见
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册