HttpRunner HttpRunner 实现参数化数据驱动机制

debugtalk · 2018年02月16日 · 最后由 ake 回复于 2018年08月03日 · 5267 次阅读
本帖已被设为精华帖!

从 1.1.0 版本开始,数据驱动机制进行了较大的优化和调整。
请参考:《HttpRunner 再议参数化数据驱动机制》

背景

在自动化测试中,经常会遇到如下场景:

1、测试搜索功能,只有一个搜索输入框,但有10种不同类型的搜索关键字;
2、测试账号登录功能,需要输入用户名和密码,按照等价类划分后有20种组合情况。

这里只是随意找了两个典型的例子,相信大家都有遇到过很多类似的场景。总结下来,就是在我们的自动化测试脚本中存在参数,并且我们需要采用不同的参数去运行。

经过概括,参数基本上分为两种类型:

  • 单个独立参数:例如前面的第一种场景,我们只需要变换搜索关键字这一个参数
  • 多个具有关联性的参数:例如前面的第二种场景,我们需要变换用户名和密码两个参数,并且这两个参数需要关联组合

然后,对于参数而言,我们可能具有一个参数列表,在脚本运行时需要按照不同的规则去取值,例如顺序取值、随机取值、循环取值等等。

对于这一块儿,没有太多新的概念,这就是典型的参数化和数据驱动。遗憾的是,当前HttpRunner并未支持该功能特性。

考虑到该需求的普遍性,并且近期提到该需求的的人也越来越多(issue #74, issue #87, issue #88, issue #97),因此趁着春节假期的空闲时间,决定优先实现下。

经过前面的场景分析,我们的目标已经很明确了,接下来就是如何实现的问题了。

借鉴 LoadRunner 的数据参数化

要造一个轮子,最好是先看下现有知名轮子的实现机制。之前有用过一段时间的 LoadRunner,对其参数化机制印象蛮深的,虽然它是性能测试工具,但在脚本参数化方面是通用的。

我们先看下在 LoadRunner 中是如何实现参数化的。

在 LoadRunner 中,可以在脚本中创建一个参数,然后参数会保存到一个.dat的文件中,例如下图中的psd.dat

.dat文件中,是采用表格的形式来存储参数值,结构与CSV基本一致。

对于单个独立参数,可以将参数列表保存在一个单独的.dat文件中,第一行为参数名称,后续每一行为一个参数值。例如本文背景介绍中的第一类场景,数据存储形式如下所示:

Keyword
hello
world
debugtalk

然后对于参数的取值方式,可以通过Select next rowUpdate value on进行配置。

Select next row的可选方式有:

  • Sequential:顺序取值
  • Random:随机取值
  • Unique:为每个虚拟用户分配一条唯一的数据

Update value on的可选方式有:

  • Each iteration:每次脚本迭代时更新参数值
  • Each occurrence:每次出现参数引用时更新参数值
  • Once:每条数据只能使用一次

而且,可以通过对这两种方式进行组合,配制出9种参数化方式。

另外,因为 LoadRunner 本身是性能测试工具,具有长时间运行的需求,假如Select next row选择为Unique,同时Update value on设置为Each iteration,那么就会涉及到参数用完的情况。在该种情况下,可通过When out of value配置项实现如下选择:

  • Abort vuser:当超出时终止脚本
  • Continue in a cyclic manner:当超出时回到列表头再次取值
  • Continue with last value:使用参数表中的最后一个值

对于多个具有关联性的参数,可以将关联参数列表保存在一个.dat文件中,第一行为参数名称,后续每一行为一个参数值,参数之间采用逗号进行分隔。例如本文背景介绍中的第二类场景,数据存储形式如下所示:

UserName,Password
test1,111111
test2,222222
test3,333333

对于参数的取值方式,与上面单个独立参数的取值方式基本相同。差异在于,我们可以只配置一个参数(例如UserName)的取值方式,然后其它参数(例如Password)的取值方式选择为same line as UserName。如此一来,我们就可以保证参数化时的数据关联性。

LoadRunner 的参数化机制就回顾到这里,可以看出,其功能还是很强大的,使用也十分灵活。

设计思路演变历程

现在再回到我们的 HttpRunner,要如何来实现参数化机制呢?

因为 LoadRunner 的参数化机制比较完善,用户群体也很大,因此我在脑海里最先冒出的想法就是照抄 LoadRunner,将 LoadRunner 在 GUI 中配置的内容在 HttpRunner 中通过YAML/JSON来进行配置。

按照这个思路,在 HttpRunner 的 config 中,就要有一块儿地方用来进行参数化配置,暂且设定为parameters吧。然后,对于每一个参数,其参数列表要单独存放在文件中,考虑到LoadRunner中的.dat文件基本就是CSV格式,因此可以约定采用大众更熟悉的.csv文件来存储参数;在脚本中,要指定参数变量从哪个文件中取值,那么就需要设定一个parameter_file,用于指定对应的参数文件路径。接下来,要实现取值规则的配置,例如是顺序取值还是随机取值,那么就需要设定select_next_rowupdate_value_on

根据该设想,在YAML测试用例文件中,数据参数化将描述为如下形式:

- config:
name: "demo for data driven."
parameters:
- Keyword:
parameter_file: keywords.csv
select_next_row: Random
update_value_on: EachIteration
- UserName:
parameter_file: account.csv
select_next_row: Sequential
update_value_on: EachIteration
- Password:
parameter_file: account.csv
select_next_row: same line as UserName

这个想法基本可行,但就是感觉配置项有些繁琐,我们可以尝试再对其进行简化。

首先,比较明显的,针对每个参数都要配置select_next_rowupdate_value_on,虽然从功能上来说比较丰富,但是对于用户来说,这些功能并不都是必须的。特别是update_value_on这个参数,绝大多数情况下我们的需求应该都是采用Each iteration,即每次脚本再次运行时更新参数值。因此,我们可以去除update_value_on这个配置项,默认都是采用Each iteration的方式。

经过第一轮简化,配置描述方式变为如下形式:

- config:
name: "demo for data driven."
parameters:
- Keyword:
parameter_file: keywords.csv
select_next_row: Random
- UserName:
parameter_file: account.csv
select_next_row: Sequential
- Password:
parameter_file: account.csv
select_next_row: same line as UserName

然后,我们可以看到UserNamePassword这两个参数,它们有关联性,但却各自单独进行了配置;而且对于有关联性的参数,除了需要对第一个参数配置取值方式外,其它参数的select_next_row应该总是为same line as XXX,这样描述就显得比较累赘了。

既然是有关联性的参数,那就放在一起吧,参数名称可以采用约定的符号进行分离。考虑到参数变量名称通常包含字母、数字和下划线,同时要兼顾YAML/JSON中对字符的限制,因此选择短横线(-)作为分隔符吧。

经过第二轮简化,配置描述方式变为如下形式:

- config:
name: "demo for data driven."
parameters:
- Keyword:
parameter_file: keywords.csv
select_next_row: Random
- UserName-Password:
parameter_file: account.csv
select_next_row: Sequential

接着,我们再看下parameter_file参数。因为我们测试用例中的参数名称必须与数据源进行绑定,因此这一项信息是不可少的。但是在描述形式上,还是会感觉有些繁琐。再一想,既然我们本来就要指定参数名称,那何必不将参数名称约定为文件名称呢?

例如,对于参数Keyword,我们可以将其数据源文件名称约定为Keyword.csv;对于参数UserNamePassword,我们可以将其数据源文件名称约定为UserName-Password.csv;然后,再约定数据源文件需要与当前YAML/JSON测试用例文件放置在同一个目录。

经过第三轮简化,配置描述方式变为如下形式:

- config:
name: "demo for data driven."
parameters:
- Keyword:
select_next_row: Random
- UserName-Password:
select_next_row: Sequential

同时该用例文件同级目录下的数据源文件名称为Keyword.csvUserName-Password.csv

现在,我们就只剩下select_next_row一个配置项了。既然是只剩一项,那就也省略配置项名称吧。

最终,我们的配置描述方式变为:

- config:
name: "demo for data driven."
parameters:
- Keyword: Random
- UserName-Password: Sequential

不过,我们还忽略了一个信息,那就是脚本的运行次数。假如参数取值都是采用Sequential的方式,那么我们可以将不同组参数进行笛卡尔积的组合,这是一个有限次数,可以作为自动化测试运行终止的条件;但如果参数取值采用Random的方式,即每次都是在参数列表里面随机取值,那么就不好界定自动化测试运行终止的条件了,我们只能手动进行终止,或者事先指定运行的总次数,不管是采用哪种方式,都会比较麻烦。

针对参数取值采用Random方式的这个问题,我们不妨换个思路。从数据驱动的角度来看,我们期望在自动化测试时能遍历数据源文件中的所有数据,那么重复采用相同参数进行测试的意义就不大了。因此,在选择Random的取值方式时,我们可以先将参数列表进行乱序排序,然后采用顺序的方式进行遍历;对于存在多组参数的情况,也可以实现乱序排序后再进行笛卡尔积的组合方式了。

到此为止,我们的参数化配置方式应该算是十分简洁了,而且在功能上也能满足常规参数化的配置需求。

最后,我们再回过头来看脚本参数化设计思路的演变历程,基本上都可以概括为约定大于配置,这的确也是HttpRunner崇尚和遵循的准则。

开发实现

设计思路理顺了,实现起来就比较简单了,点击此处查看相关代码,就会发现实际的代码量并不多。

在这里我就只挑几个典型的点讲下。

数据源格式约定

既然是参数化,那么肯定会存在数据源的问题,我们约定采用.csv文件格式来存储参数列表。同时,在同一个测试场景中可能会存在多个参数的情况,为了降低问题的复杂度,我们可以约定独立参数存放在独立的.csv文件中,多个具有关联性的参数存放在一个.csv文件中。另外,我们同时约定在.csv文件中的第一行必须为参数名称,并且要与文件名保持一致;从第二行开始为参数值,每个值占一行。

例如,keyword这种独立的参数就可以存放在keyword.csv中,内容形式如下:

keyword
hello
world
debugtalk

usernamepassword这种具有关联性的参数就可以存放在username-password.csv中,内容形式如下:

username,password
test1,111111
test2,222222
test3,333333

csv 解析器

数据源的格式约定好了,我们要想进行读取,那么就得有一个对应的解析器。因为我们后续想要遍历每一行数据,并且还会涉及到多个参数进行组合的情况,因此我们希望解析出来的每一行数据应该同时包含参数名称和参数值。

于是,我们的数据结构就约定采用list of dict的形式。即每一个.csv文件解析后会得到一个列表(list),而列表中的每一个元素为一个字典结构(dict),对应着一行数据的参数名称和参数值。具体实现的代码函数为_load_csv_file

例如,上面的username-password.csv经过解析,会生成如下形式的数据结构。

[
{'username': 'test1', 'password': '111111'},
{'username': 'test2', 'password': '222222'},
{'username': 'test3', 'password': '333333'}
]

这里还会涉及到一个问题,就是参数取值顺序。

YAML/JSON测试用例中,我们会配置参数的取值顺序,是要顺序取值(Sequential)还是乱序随机取值(Random)。对于顺序的情况没啥好说的,默认从.csv文件中读取出的内容就是顺序的;对于随机取值,更确切地说,应该是乱序取值,我们需要进行一次乱序排序,实现起来也很简单,使用random.shuffle函数即可。

if fetch_method.lower() == "random":
random.shuffle(csv_content_list)

多个参数的组合

然后,对于多个参数的情况,为了组合出所有可能的情况,我们就需要用到笛卡尔积的概念。直接看例子可能会更好理解些。

例如我们在用例场景中具有三个参数,a为独立参数,参数列表为[1, 2];xy为关联参数,参数列表为[[111,112], [121,122]];经过解析后,得到的数据分别为:

a:
[{"a": 1}, {"a": 2}]

x & y:
[
{"x": 111, "y": 112},
{"x": 121, "y": 122}
]

那么经过笛卡尔积,就可以组合出4种情况,组合后的结果应该为:

[
{'a': 1, 'x': 111, 'y': 112},
{'a': 1, 'x': 121, 'y': 122},
{'a': 2, 'x': 111, 'y': 112},
{'a': 2, 'x': 121, 'y': 122}
]

这里需要强调的是,多个参数经过笛卡尔积运算转换后,仍然是list of dict的数据结构,列表中的每一个字典(dict)代表着参数的一种组合情况。

参数化数据驱动

现在,我们已经实现了在YAML/JSON测试用例文件中对参数进行配置,从.csv数据源文件中解析出参数列表,并且生成所有可能的组合情况。最后还差一步,就是如何使用参数值来驱动测试用例的执行。

听上去很高大上,但实际却异常简单,直接对照着代码来说吧。

对于每一组参数组合情况来说,我们完全可以将其视为当前用例集运行时定义的变量值。而在 HttpRunner 中每一次运行测试用例集的时候都需要对runner.Runner做一次初始化,里面会用到定义的变量(即config_dict["variables"]),那么,我们完全可以在每次初始化的时候将组合好的参数作为变量传进去,假如存在同名的变量,就进行覆盖。

这样一来,我们就可以使用所有的参数组合情况来依次驱动测试用例的执行,并且每次执行时都采用了不同的参数,从而也就实现了参数化数据驱动的目的。

效果展示

最后我们再来看下实际的运行效果吧。

假设我们有一个获取token的接口,我们需要使用 user_agent 和 app_version 这两个参数来进行参数化数据驱动。

YAML 测试用例的描述形式如下所示:

- config:
name: "user management testset."
parameters:
- user_agent: Random
- app_version: Sequential
variables:
- user_agent: 'iOS/10.3'
- device_sn: ${gen_random_string(15)}
- os_platform: 'ios'
- app_version: '2.8.6'
request:
base_url: $BASE_URL
headers:
Content-Type: application/json
device_sn: $device_sn

- test:
name: get token with $user_agent and $app_version
api: get_token($user_agent, $device_sn, $os_platform, $app_version)
extract:
- token: content.token
validate:
- "eq": ["status_code", 200]
- "len_eq": ["content.token", 16]

其中,user_agent 和 app_version 的数据源列表分别为:

user_agent
iOS/10.1
iOS/10.2
iOS/10.3
app_version
2.8.5
2.8.6

那么,经过笛卡尔积组合,应该总共有6种参数组合情况,并且 user_agent 为乱序取值,app_version 为顺序取值。

最终的测试结果如下所示:

$ hrun tests/data/demo_parameters.yml

Running tests...
----------------------------------------------------------------------
get token with iOS/10.2 and 2.8.5 ... INFO:root: Start to POST http://127.0.0.1:5000/api/get-token
INFO:root: status_code: 200, response_time: 13 ms, response_length: 46 bytes
OK (0.014845)s
get token with iOS/10.2 and 2.8.6 ... INFO:root: Start to POST http://127.0.0.1:5000/api/get-token
INFO:root: status_code: 200, response_time: 2 ms, response_length: 46 bytes
OK (0.003909)s
get token with iOS/10.1 and 2.8.5 ... INFO:root: Start to POST http://127.0.0.1:5000/api/get-token
INFO:root: status_code: 200, response_time: 3 ms, response_length: 46 bytes
OK (0.004090)s
get token with iOS/10.1 and 2.8.6 ... INFO:root: Start to POST http://127.0.0.1:5000/api/get-token
INFO:root: status_code: 200, response_time: 5 ms, response_length: 46 bytes
OK (0.006673)s
get token with iOS/10.3 and 2.8.5 ... INFO:root: Start to POST http://127.0.0.1:5000/api/get-token
INFO:root: status_code: 200, response_time: 3 ms, response_length: 46 bytes
OK (0.004775)s
get token with iOS/10.3 and 2.8.6 ... INFO:root: Start to POST http://127.0.0.1:5000/api/get-token
INFO:root: status_code: 200, response_time: 3 ms, response_length: 46 bytes
OK (0.004846)s
----------------------------------------------------------------------
Ran 6 tests in 0.046s

至此,我们就已经实现了参数化数据驱动的需求。对于测试用例中参数的描述形式,大家要是发现还有更加简洁优雅的方式,欢迎反馈给我。

最后,本文发表于 2018 年大年初一,祝大家新年快乐,狗年旺旺旺!

附言 1  ·  2018年03月21日

中文使用说明文档:http://cn.httprunner.org/data-driven/

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

大年初一发的文章肯定没人看😅

debugtalk 回复

刚看了点👐

给勤劳的童鞋点个赞😀

你可真活跃,还用不用过年了

heyniu 回复

今年春节没回老家😅

seveniruby 将本帖设为了精华贴 02月23日 17:31

请问运用har2case报错如图片是什么原因

Bingcc 回复

你把抓包得到的 test.har 发给我看下

楼主好,我想知道我在哪里可以查看测试用例中都有哪些标志数据的,比如base_url,hearder,times.
比如times是我在翻阅开发历程文档中看到的,不然根本不知道有这个功能,然而我在使用文档中并没有找到一个统一有哪些关键数据的文档的

dreamer.li 回复

http://cn.httprunner.org/testcase-structure/

还在写,近期会完成整理

赞赞赞,向勤劳的人致敬!

公司新项目一直在用httprunner,我也根据自己的需求,修改源码。现在遇到一个前端问题:在生成测试报告的时候,使用的是pyunitreport里面的模板,模板里面调用bootstrap.min.css, 路径写的是internet上的url. 问题是:公司不允许上外网,所以这个链接找不到。我下好了bootstrap.min.css, 放在pyunitreport安装包里.
问题:模板中的路径如何写,才能获取到每个用户的安装路径?或者能获取到用户的环境变量也可以.
report_template.html
!----
我目前写成这样的 相对路径,但实际是以生成测试报告的目录为当前路径,而不是安装包的路径.
目前我有两个 思路 :1,做 个站点,所有用户从 站点访问,把所有需要的资源放入站点
2,临时 解决问题,把css放入每个用户都有的目录,比如 system32/调用的地方写绝对路径
是否 有其它好的实现方式?

楼主好,我 想知道如果接口的请求值和返回值都是加密后的,怎么来使用httprunner。

seeyouseeme829 回复

感觉你用的 HttpRunner 版本有点老了,你升级到最新版 1.1.0 吧;pyunitreport 已经废弃掉了的。

oldshen 回复

1、请求若有加密,可自定义加密算法,然后进行引用;
2、响应若有加密,可使用状态码作为校验。

有四个问题,不知道楼主能不能回答下:
1、用例标签是否后期能加上
2、如果我的请求data中不是json格式的怎么处理。比如我的是data 格式的
3、session及cookies是默认的吗
4、tearDownClass类似功能怎么加?

vioub 回复

1、用例标签可以加,可否到 GitHub 上提个 issue,详细描述下需求形式;
2、HttpRunner 底层完全基于 Python Requests,因此用法也可以直接参考 Requests 的请求方法;

"test": {
"name": "/manycc/enquiry/getPriceByCity中文",
"request": {
"url": "http://yuncc.changjiu56.com/manycc/enquiry/getPriceByCity",
"headers": {
"If-Modified-Since": "Sat, 24 Feb 2018 01:51:41 GMT+00:00",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
},
"method": "POST",
"data": "from_addr=北京&to_addr=石家庄市"
}
}

3、同一个测试用例集中,采用的是同一个 requests.Session,因此 session 和 cookies 是共享的;
4、hook 机制 可参考文档:http://cn.httprunner.org/request-hook/

debugtalk 回复

响应加密,只判断状态码好像有点不太合适,是不是可以定义一下返回的方式,解密好在去解析。

songxiang 回复

支持自定义 comparator 的,你可以将整体响应结果传入 自定义的 comparator,在里面解密后进行判断。

debugtalk 回复

httprunner 能支持多线程运行接口测试吗

holysor 回复

并发执行实现压测是可以的。但你要是说的将一组测试用例集分配给多个线程同时执行的话,暂时还没有支持。

debugtalk 回复

见过的国内写得最好最清晰的说明文档!!!

vic 回复

过奖啦😂

请问大佬有没有可以检查yaml格式的机制,因为统一执行测试用例時,如果某个yaml文件格式错误,就会报解析错误,自己是否可以写脚本实现自动检查yaml格式,并且可以自动给出哪个文件哪行格式错误

zhuzhu_mx 回复

HttpRunner 从 1.3.1 版本开始,支持对 JSON 格式测试用例的内容进行格式正确性检测和样式美化功能。
http://cn.httprunner.org/testcase/validate-pretty/

对于YAML格式,的确还没有很好的工具支持。


你好楼主,我将登陆返回的code和desc(含有中文)写入csv文件里面,然后与返回值进行判断,这样可以吗?我这边会报
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xb9 in position 121: invalid start byte

还有一个,csv里面的code是str类型,实际返回的却是int类型,也会报错

caige 回复

parameters 是用于数据驱动,并不是定义变量。
仔细读下文档吧。

尝试了下,还是不错的,支持!

zhishui 回复

多谢支持

32楼 已删除
debugtalk 回复

谢谢楼主,认真又看了下,问题找到了,现在可以了。

34楼 已删除
debugtalk 回复


楼主你好,有一种情况,如果返回的结果中没有token(包含在content里面)这个参数,那content.token就会报错了,这种情况有没有解决方法,如下

demo-quickstart-3.yml 这个文件没有将base_url 分离出来,文件本身是不对的

FrancisJen 回复

没有问题,你运行也是可以正常运行的。

caige 回复

如果返回结果中的某个字段不能保证一定存在,那么也没有必要对其进行校验吧。校验内容肯定是要明确的。

大佬,有个问题想请教一下,利用数据驱动实现了接口入参的多样性,如果想针对不同的输入参数对接口返回的同一个值做不同的断言,怎么实现呢,看你的例子里,接口的断言比较简单,只是检验了code=200,没有对接口的返回做差异化的校验

Jesmine 回复

断言支持使用自定义函数。如果你的响应和输入是存在规则关联的,你可以实现一个校验函数来达到你的目的。

  • config: name: testset description parameters: - user_id: [1001, 1002, 1003, 1004] variables: - device_sn: ${gen_random_string(15)} - user_id: 1000 request: base_url: http://127.0.0.1:5000 headers: User-Agent: python-requests/2.18.4 device_sn: $device_sn Content-Type: application/json

上面的varialbles中user_id和参数中的user_id是不是重复定义了, 下面的test中使用的是哪个呢?

42楼 已删除

以前觉得RF做接口就很好了,用了大佬debugtalk的httprunner,才发现爽的简直一哔,每日学习中

along8846 回复

多谢如此清新脱俗的夸奖

仅楼主可见

@BensonMax 你在前一个 test 中进行了参数化,那么在运行的时候,前一个 test 就会运行多次,运行完成后才再运行第二个 test。
如果你想的是使用每一组参数化数据分别运行两个 test,那你应该将 parameters 放置到 config 中。

debugtalk 回复


httprunner 有断言转义方法? 还是需要自己写自定义函数?

仅楼主可见

@riku5596 当前不支持,详情可阅读 遗留问题

BensonMax 回复

可否贴下你实际的返回内容?

debugtalk 回复

期待大神解决这个问题啊,实在太需要了

再问一下:看了遗留问题,如果愿意牺牲统计数据(仅牺牲用例个数的准确性),这个用例该怎么修改呢?

仅楼主可见
BensonMax 回复

从你的截图来看,实际值就应该是int类型,那么你的expect也应该是int类型;你要么在指定expect的时候使用int,要么自己自定义一个校验函数,然后在函数里去对比。

用例执行前后的setup和teardown,看了手册之后,对于setup_hook,和teardown_hook这部分还是不太明白。
比如我调用接口结束后,想做清空数据库的操作,我在debugtalk.py中写个方法请数据库,然后怎么使用teardown_hook去使用这个清库方法呢

还是感觉不太好用啊,也不知道是不是我弄错了。就是从csv取出来的字典组,不是一个变量值,怎么用呢,我自己写了个遍历的函数,感觉没有用输出来的value是这样的: ${get_value(${parameterize(payFormName.csv)}, 0)} 而不是它的值。。。我也很郁闷

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