一、 背景:

为了让大家更加的了解Jmeter,并且使用起来游刃有余。
这篇我们主要讲一下,如何优雅的使用 Jmeter 一步步的实现接口自动化,完成脚本与数据分离,把可能对 Jmeter 脚本的维护转移到 csv 文本中,降低接口变更时对脚本的维护,最终目标是实现写好接口自动化脚本后,接口变更的维护都只要操作 csv 文件。

Jmeter 脚本,数据和报告地址:
https://github.com/grizz/jmeter-master

二、实例

先介绍一个 Jmeter 的函数-》csvRead 函数,后续介绍使用的会比较多,熟悉的伙伴可以直接跳过。

1、csvRead 函数使用:

csvRead 函数是从外部读取参数,可以从一个文件中读取多个参数。
使用步骤:
1、先新建一个文件,例如test.csv(或 test.txt),里面的数据存放为
grizz,qq1111
jiezai,qq1111

文件为用户名和密码,用逗号隔开,每一列表示一种参数,每一行则表示一组参数。

2、选项-》函数助手对话框-》函数助手,打开 Jmeter 的函数助手,选择 csvRead 函数:

其中:
CSV file to get values from | *alias:要读取的文件路径,为绝对路径
CSV 文件列号 | next| *alias:从第几列开始读取,注意第一列是 0

${__CSVRead(D:/test.csv,0)} 取到的值为 grizz
${__CSVRead(D:/test.csv,1)} 取到的值为 qq1111

3.Jmeter 执行的时候,如果有多个线程,顺序读取每行的数据,如果线程组多于文件中的行数,则循环读取。如线程数为 2,则第 2 个线程读取的是第二行的数据,线程数为 3,线程数 3 大于文件中的行数 2,则第 3 个线程读取的是第一行的数据。

PS:这一函数并不适合于读取很大的文件,因为整个文件都会被存储到内存之中。对于较大的文件,请使用配置元件 CSV Data Set 或者 StringFromFile 。但是我们不是压测,只是接口自动化,一般没有太大的数据文件,啊哈哈哈哈。

默认情况下,函数会在遇到的每一个逗号处断行,需要换一个分隔符(通过设置属性 csvread.delimiter 来实现)
修改jmeter.properties文件:

#csvread.delimiter=,
修改为
csvread.delimiter=?

即把分隔符修改为?问号,注意前面的 # 号代表注释,要去掉。重启 Jmeter 生效。

2、使用的接口:

这里我们以两个接口举例,其中Content-Type=application/json
1·获取 token 的接口/getToken
入参:
{
"flag":"test",
"appId":"001"
}
返回值:
{"returnFlag":"1000","returnMsg":"获取 token 成功","token":"19940622"}

2·使用 token 的接口/useToken,主要是测试 useToken 接口,useToken 接口的 token 需要从 getToken 接口的返回值中取,其实就是参数关联。
入参:
{
"flag":"${token}",
"appId":"001"
}
返回值,以 3 个场景为例:
{"returnFlag":"1000","returnMsg":"使用 token 成功"}
{"returnFlag":"1001","returnMsg":"token 为空"}
{"returnFlag":"1002","returnMsg":"token 错误"}

对于接口的断言,我们默认"returnFlag":"1000"即接口业务正常返回,10011002代表接口针对业务的不同异常给予的返回,当然也在我们的接口测试范围内。

我们分v1v2v3v4,4 个版本循序渐进的讲:

v1 版本:

刚开始入门时,我们的脚本可能会是这样的
先请求 getToken 接口,并获取 token,用正则表达式提取如下

再请求 useToken 接口,flag 的值输入 ${token},使用提取到的 token 值。
入参:{"flag":"${token}","appId":"001"}
返回值:{"returnFlag":"1000","returnMsg":"使用 token 成功"}
断言:"returnFlag":"1000",断言成功

再测试后续两种场景,入参如下:
sampler-使用 tokenv1-为空:
入参:{"flag":"","appId":"002"}
返回值:{"returnFlag":"1001","returnMsg":"token 为空"}
断言:"returnFlag":"1000",断言失败

sampler-使用 tokenv1-错误:
入参:{"flag":"errorToken","appId":"003"}
返回值:{"returnFlag":"1002","returnMsg":"token 错误"}
断言:"returnFlag":"1000",断言失败
查看结果树

这一顿操作下来,没啥问题,因为 useToken 接口的 3 种场景我们都覆盖了,只要把异常的场景的断言对应改一下,我们的接口脚本就写好了,可以交付。但是如果我们要实现接口自动化,那么 v1 版本中sampler 的重复率比较高,我们考虑能不能把 useToken 的 3 种场景放到一个sampler中。
于是有了 v2 版本

v2 版本:

由于聪明的我们有一定前瞻性,我们知道,想降低脚本中sampler 的重复率,需要借助数据文件,我们可以把接口 useToken 的请求 body 直接从文件中读

入参1:{"flag":"${token}","appId":"001"}
入参2:{"flag":"","appId":"002"}
入参3:{"flag":"errorToken","appId":"003"}

入参 2,3 我们可以从文件中读取没问题,但是入参 1 这样从文件中读取,”${token}” 读取出来的值就是字符串”${token}”,而不是我们想要的” 19940622”,怎么办呢?
于是我们思考着,可以把接口 useToken 的请求分两种情况,需要正确的 token 和不需要正确的 token,如果需要正确的 token,我们就在 Jmeter 中传给他,其它参数还是可以在文本中读取;如果不需要正确的 token,则请求全部从文件中读取。什么意思呢,继续往下看。

我们设置存放 csv 文件的目录 DATA=/jmeter/testcase,因为我们可能会多次使用到这个目录,所以可以用${ DATA }代表我们的文件目录。

useToken_v2.csv文件:

用例1-token正确?1?"appId":"001"}
用例2-token为空?0?{"flag":"","appId":"002"}
用例3-token错误?0?{"flag":"errorToken","appId":"003"}

举例:
${__CSVRead(${DATA}/useToken_v2.csv,1)}依次取到的是 1,0,0
${__CSVRead(${DATA}/useToken_v2.csv,2)}第二次取到的是{"flag":"","appId":"002"}

如果我们useToken_v2.csv文件有 3 行,则设置 auto_interface_v2 线程数为 3。
因为对于 csvRead 函数,每一个线程都有独立的内部指针指向文件数组中的当前行。当某个线程第一次引用文件时,函数会为线程在数组中分配下一个空闲行。如此一来,任何一个线程访问的文件行,都与其他线程不同(除非线程数大于数组包含的行数)。
当我们需要正确 Token 时,我们利用 if 控制器,如果文本的第二列 (我这里是以问号分隔的,因为默认是逗号,但是我们接口 json 数据本身就有逗号,只能换一个) 为 1,则 flag 从 Jmeter 中自己读取;如果文本的第二列为 0 则代表不需要正确 token,那接口入参我们就全部从文件中读取。
If:${__CSVRead(${DATA}/useToken_v2.csv,1)}==1

sampler-使用 tokenv2-haveToken 的请求 body 为:

{"flag":"${token}", ${__CSVRead(${DATA}/useToken_v2.csv,2)}
即请求 body 由两部分组成,一部分来自 Jmeter 内,主要是获取正确的 token,另一部分来自useToken_v2.csv文件的第 3 列。
我们这里 token 正确时,另一个参数好像只有"appId":"001"这一种情况,显得好像这样写起来更加冗余,其实不然,我们 appId 也有可能为空,也可能错误。这时候我们的useToken_v2.csv文件应该会是这样:

用例1-token正确?1?"appId":"001"}
用例2-appId为空?1?"appId":""}
用例3-appId错误?1?"appId":"error appId "}
用例4-token为空?0?{"flag":"","appId":"002"}
用例5-token错误?0?{"flag":"errorToken","appId":"003"}

而且一个接口的入参不可能只有两个,但一般 token 只会有一个,所以当接口参数多时,这样写还是减少了一定量的sampler 的重复率

If:${__CSVRead(${DATA}/useToken_v2.csv,1)}==0
请求 body 就为:${__CSVRead(${DATA}/useToken_v2.csv,2)}

再解释一下这里为什了加了一个计算器,和接口名称为什么命名为使用tokenv2-noToken-${_number}
首先,当我们把线程数设置为 3 时,其实 getToken 接口也跑了 3 次,但是其实我们只需要它跑一次,取出正确的 token 就可以了,getToken 接口的 if 控制器跟计算器一起作用,当第 1 个线程启动时触发 if 控制器的规则${_number}==1,第 2,第 3 个线程是就不会触发 if 控制器里面的 getToken 接口。

useToken 接口命名后面加一个 ${_number},
使用 tokenv2-noToken-${_number}和使用 tokenv2-haveToken-${_number};主要是当接口报错时,可以根据接口的名称 (其后面加了 ${_number},接口每个场景 ${_number}都是不一样的),判断其对应useToken_v2.csv文件的哪一行导致报错,可快速定位并进行报错修改。
方便理解,给出运行效果图如下:

v3 版本:

v2 版本我们好像把我们能做的都给做了,但是前提是
从文件中读取,”${token}” 读取出来的值就是字符串”${token}”,而不是我们想要的” 19940622”!
随着时间的推移,楼主真的前前后后看过网上各种 Jmeter 教程和使用,不下 40 次,毕竟自己一直有信念,别人能做的自己也可以,1 次看不懂的,那就看 5 次,5 次还不懂,10 次。不努力,没有办法比别人做得更好的。接着说随着时间的推移,grizz 发现那个前提是可以打破的,因为 Jmeter 有个eval函数。
函数__eval可以用来执行一个字符串表达式,并返回执行结果。
举个栗子:
name=grizz
SQL=select * from able where name='${name}'
${ SQL }=select * from able where name='${name}'
${__eval(${SQL})}= select * from able where name='grizz'

现在我们可以设置useToken_v3.csv文件如下:

用例1-token正确?{"flag":"${token}","appId":"001"}?"returnFlag":"1000"
用例2-token为空?{"flag":"","appId":"002"}?"returnFlag":"1000"
用例3-token错误?{"flag":"errorToken","appId":"003"}?"returnFlag":"1000"

明显的,我们不需要 if 控制器来判断是否需要正确的 Token 了,脚本看起来清新了一些,而且我们把响应断言也从文件中读取。这样开发修改接口的返回提示时,我们可以直接通过修改 csv 文件完成对应的修改,不需要去动 jmeter 脚本。
入参${__eval(${__CSVRead(${DATA}/useToken_v3.csv,1)})}
断言${__CSVRead(${DATA}/useToken_v3.csv,2)}

看到这我们可以思考一下,在 v4 版本还有哪些内容可以优化?

v4 版本:

v1 到 v2 是入门到掌握,v2 到 v3 应该是弱鸡到熟悉,v4 应该到星耀了吧,很想写个 v5 版本,v5(威武),应该很强的怕。
我们想想,我们的最终目标是实现写好接口自动化脚本后,接口变更的维护都只要操作 csv 文件。
那么当我们的 useToken 接口新增了一个场景,token 过期

useToken_v4.csv文件

用例个数?4
用例1-token正确?{"flag":"${token}","appId":"001"}?"returnFlag":"1000"
用例2-token为空?{"flag":"","appId":"002"}?"returnFlag":"1000"
用例3-token错误?{"flag":"errorToken","appId":"003"}?"returnFlag":"1000"
用例4-token过期?{"flag":"oldToken","appId":"003"}?"returnFlag":"1000"

设置线程组 auto_interface_v4 的线程数为
${__CSVRead(${DATA}/useToken_v4.csv,1)},其实就是文件useToken_v4.csv第一行的第二列,就是 4

因为我们的线程数与我们的文件行数挂钩,但这里为什么文件有 5 行,而线程数是 4。前面说了,因为对于 csvRead 函数,每一个线程都有独立的内部指针指向文件数组中的当前行。当某个线程第一次引用文件时,函数会为线程在数组中分配下一个空闲行。即 Jmeter 会分配一个线程去保存线程的属性,如线程数,启动时间,循环次数等。即当线程数设置为${__CSVRead(${DATA}/useToken_v4.csv,1)}时,控制线程属性的线程就读取了useToken_v4.csv文件的第一行。

PS:再说一下 csvRead 函数
csvRead 函数默认从文件第一行开始读取,除非你二次开发,不然这个默认就一只在 (苦笑),就是说我们不好加文件列的标题 (当然可以利用 v4 版本的第一行也行),降低了文件的可读性。但当对某个文件进行第一次读取时,文件将被打开并读取到一个内部数组中。如果在读取过程中找到了空行,函数就认为到达文件末尾了,即允许拖尾注释。就是我们可以在文件写完后回车一下,再写一些文件的注释,或者${__CSVRead(D:/test.csv,next)}了解一下,csvRead 函数的第二个值为 next 可自行进行文件行标的切换。

其实当我们的脚本量化后,还有很多东西要考虑的,接口 csv 文件的命名规范,线程组和 sampler 的命名规范,因为线程组和 sampler 的名称会在报告中体现,接口入口和出口标准,怎么维护我们的数据和脚本更优雅,怎样更高效的运行脚本和生成更丰富的报告,更加的节省测试人力。

下一篇讲一下 jemter 和 ant,jenkins 的持续集成
Jmeter+ant+Jenkins 接口自动化框架完整版
接口汇总报告:

接口详细报告:

欢迎交流指正,感谢阅读。


↙↙↙阅读原文可查看相关链接,并与作者交流