文章内容是参照Jmeter官网和自己实践完成的,JMeter官网地址贴上,有兴趣的朋友可以去阅读一下:JMeter官网
本文基于JMeter5.1.1版本。
TestPlan(测试计划)是使用 JMeter 进行测试的起点,也是其它 JMeter 测试元件的容器。一个JMeter脚本有且只有一个测试计划。
线程组元件是一个测试计划的起点,测试计划的所有元件都要包含在线程组中。一个测试计划中可以有多个线程组。
Jmeter提供了多种逻辑控制器,下面将会对它们的作用和用法做详解。对于下文中多次使用的【BeanShell Sampler】,后续会详解其用法,这里先把它简单地看做能返回特定的值的一个请求即可。
If控制器,在这个控制器中,我们可以写一些条件表达式,表达式结果为true,则该控制器下的子项会被执行,否则不执行。
在5.1.1版本中,【Interpret Condition as Variable Expression】是默认勾选的,意为【将条件解释为变量表达式】。在这种模式下,1!=1、1==1等条件表达式是会被一律判定为false的,这点要注意。
警告标识旁子句的意思是:为了提升性能起见,建议勾选【Interpret Condition as Variable Expression】,并使用_jexl3 或 _groovy 去评定条件结果是true还是false。
一些变量表达式的示例:
${__jexl3(${COUNT} < 10)}
:变量表达式判断COUNT
这个变量小于10(COUNT
是此控制器前被定义并赋值的);${__jexl3(1 == 1)}
:变量表达式判断 1==1是否成立;${__groovy(vars.get("myVar") != "Invalid" )}
:判断myVar不是无效的;${__groovy(vars.get("myInt").toInteger() <=4 )}
:判断myInt这个变量小于等于4;${__groovy(vars.get("myMissing") != null )}
:判断myMissing这个变量不是null;${RESULT}
:RESULT这个变量本身的值应该是true或者false;${JMeterThread.last_sample_ok}
:判断最后一个sample是否成功;例如:
这种情况下,IF控制器判定为true,HTTP请求1和HTTP请求2都会被执行。
我们再看下【条件】这个输入项。它支持哪些方式:
条件表达式,例如1!=1、2>1
,或者${var}>0
,"${var}"=="abcd"
(对于String
,"\=="
前面的双引号不能省略)这样的写法,但这种写法在内部使用javascript
来判断【条件】的计算结果是true
还是false
,可能会造成性能损失。在勾选了【Interpret Condition as Variable Expression】后,这些表达式的结果会被一律判定为false。
例如,下面这个脚本,先通过【用户定义的变量】定义一个变量myVar,赋值为1:
【如果控制器】中取消勾选【Interpret Condition as Variable Expression】,写入条件表达式:
运行脚本,查看结果树,HTTP请求1和HTTP请求2都被执行。
当然,更好的选择是使用变量表达式,当然前提是勾选【Interpret Condition as Variable Expression】(不勾选也能使用,但影响性能)
判断循环控制器,作用是循环运行其子项,直到条件为false。这个控制器和Java中的while语法是很相似的,和【if控制器】用起来,在Condition
上有相似之处。
【Condition】可以填入的值有:
1、空白:【While控制器】下的最后一个请求(也就是图中的HTTP请求3)失败,退出循环。
2、LAST:【While控制器】下的最后一个请求(也就是图中的HTTP请求4)失败,退出循环。如果在进入【While控制器】前的最后一个HTTP请求失败了,那么【While Controller】将不会执行。
3、其它:表达式结果为false
时,退出循环。有以下情形:
${myVar}
:变量myVar
在其它项中被赋值为false
;${__javaScript(${C}==10)}
:针对数字型变量进行对比判断,这种表达式的计算结果为false
时不进入或者退出循环;${__javaScript("${C}"=="abc")}
:针对字符串类型变量进行对比判断,这种表达式的计算结果为“false”时不进入或者退出循环(区别在于双引号);${__javaScript("${VAR2}"=="abcd")}
:VAR2在其它项中被赋值与“abcd”做比较,不相等则退出循环;${_P(property)}
:属性被其它项目赋予false
简单控制器是最基本的控制器,作用是将请求分组归集在一个简单控制器中,可以理解成一个模块,使得脚本结构更清晰。对JMeter脚本运行没有实质上的影响。
循环控制器,这个控制器的作用是使其子项循环运行。
循环次数(Loop Count):在输入框中输入需要循环的次数,控制器下的子项会循环相应的次数。如果勾选了【forever】,那么控制器下的子项会一直运行。
仅一次控制器,会使该控制器下的子项每个线程只运行一次。
在线程组下添加一个仅一次控制器,该控制器下添加一个HTTP请求1,线程组本身添加一个HTTP请求2,并将线程组循环次数设置为4,如下:
结果如下,符合我们的预期,仅一次控制器下的HTTP请求只会运行一次,无论线程组循环多少次:
那么,如果脚本多线程并发呢?我们将线程组的线程数设置为2,再次运行脚本:
可见,【仅一次控制器】线程间是隔离的,每个线程启动后,会运行一次【仅一次控制器】。
如果,【仅一次控制器】和【循环控制器】结合起来呢?我们将【循环控制器】循环次数设为3,并在其下面添加【仅一次控制器】,脚本结构见下图。运行后的结果如下:
可见,【仅一次控制器】在【循环控制器】中同样生效。
交替控制器,使得该控制器包含的取样器步骤在每次循环中交替执行。
例如,下面的脚本中,线程组循环次数设为3,如果没有【交替控制器】,那么HTTP请求1和HTTP请求2将会各运行3次。
运行脚本,发现HTTP请求1和HTTP请求2有了交替执行的效果:
再看交替控制器下的两个参数项。
首先验证【Ignore sub-contorller blocks】的作用,建立如下脚本,线程组循环次数设为5次:
不勾选【忽略子控制模块】,结果是这样的:
勾选【忽略子控制模块】,结果是这样的:
区别很明显,勾选了【Ignore sub-contorller blocks】后,交替控制器子控制器中的取样器一次运行只会被执行一个。
随机控制器,当该控制器下有多个子项时,每次循环会随机执行其中一个。
建立下图的脚本结构,线程组【循环次数】设置为2。运行脚本,结果如下,脚本第一次运行执行了HTTP请求4,第二次运行执行了HTTP请求3,符合预期。
随机控制器有一个参数项:Ignore sub-controller block(忽略子控制器模块)。如果勾选了此项,随机控制器下的子控制器中的多个子项只会被执行一个。
修改脚本结构,线程组【循环次数】为2,运行脚本,结果两次都随机到了【简单控制器1】:
勾选【Ignore sub-controller block】后再次运行脚本,两次分别随机到了简单控制器1和2,并执行了其中一个HTTP请求(进入简单控制器内是按顺序执行HTTP请求的,即第一次进入简单控制器肯定会执行该控制器下第一个请求):
随机顺序控制器,与简单控制器类似,会执行它下面的每个子项,只不过执行顺序是随机的。
吞吐量控制器,允许用户自行调整该控制器下的子项的执行频率。
吞吐量控制器有两种模式:
1、Total Executions:当该控制下的子项被执行固定数量后,停止吞吐量控制器。例如下面这个脚本,线程组【循环次数】设为6,运行脚本。
【吞吐量控制器】下的HTTP请求1只运行了3次:
2、Percent Executions:百分比模式,该模式使吞吐量控制器下的子项执行总循环次数的一定比例(在吞吐量中设置该比例),例如下面的脚本。
设置线程组【循环次数】为50,运行后,查看聚合报告,吞吐量控制器下的HTTP请求1执行了30次,也就是(50*60%)次。
运行周期控制器,顾名思义,这是一种设置运行时间的控制器,它的效果就是使该控制器下的子项运行时间为【Runtime】中的数值(单位:s)。
运行脚本,右上角显示脚本运行的时间,与预设一致。
不过,经过实测,如果线程组的循环次数勾选“永远”,则HTTP请求会一直运行,如果循环次数填入1,则HTTP请求会运行3s,循环次数填入2的话,HTTP请求运行6s,因此可知,在线程组不勾选“永远”的前提下,【Runtime Controller】的运行时间为【Runtime】的值乘以线程组循环次数。
开关控制器,通过【Switch Value】来控制哪个子项被执行,作用和Java中的switch语法是很类似的。
【Switch Value】有两种赋值方式:索引和子项名,经过实际测试,如果填入数字,且子项中有以数字命名的子项(当然,实际工作中要尽量避免这种命名方式),索引优先生效。
例如下面这个脚本中,Switch value是3,以3为索引和以3为名称分别可以匹配到一个HTTP请求取样器,但索引优先。
将线程组循环次数设为5,运行脚本,结果索引为3的HTTP请求4(索引从0开始)运行了5次。
如果将Switch value改为7,将3这个HTTP请求取样器改名为7,再次运行脚本:
有人会觉得这功能太鸡肋了吧,其实【Switch Controller】配合其他组件,才会更有意义,比如说和【Bean Shell】配合:
import java.util.Random;
Random random = new Random();
int index = random.nextInt(4);
vars.put("index",String.valueOf(index));
上图中的【Bean Shell】的作用是返回一个名称为“index”,值为[0,4]区间的随机int。
【Switch Value】填入${index}
,运行脚本:
遍历循环控制器,首先看下它的各输入项:
host
,为什么要写host
呢?这是因为【用户定义的变量】中变量名称是host
为前缀的,前缀是指数字前面的内容。当然这个变量还可以来自【正则表达式提取器】、【参数化】等。用户定义的变量中设置的变量:
HTTP请求按下图写入,来验证ForEach Controller的作用。
运行脚本,发现HTTP请求被执行了三次(end-start的值):
模块控制器,可以理解为对封装好的模块的调用。
观察上图的脚本结构并运行,查看结果树,可以看到,线程组1中的模块控制器可以调用线程组2中的简单控制器3及其下面的sampler。
由此可知,模块控制器的作用在于,当一个测试片段(通常是一个包含sampler的控制器)在脚本中多处运行时,模块控制器可以非常便利地完成调用,避免重写这个测试片段,使脚本减少冗余,结构简洁。 另外,当测试计划中有多个线程组时,一个线程组需要运行其它线程组的一个测试片段,模块控制器的作用就更加明显了。在这种场景下,即使其它线程组被禁用,依然不影响模块控制器对其节点下测试片段的调用。而在实际测试工作中,通常是一个线程组启用,而其它线程组被禁用,防止线程组互相干扰。 使用模块控制器时,需要注意的是,要保证控制器的名字各不相同,因为模块控制器是通过控制器名去调用的。
包含控制器,它的作用是引入外部的jmx文件。需要注意的有以下几点:
事务控制器,生成一个额外的采样器来测量其下测试元素的总体时间;值得注意的是,这个时间包含该控制器范围内的所有处理时间,而不仅仅是采样器的。由于时钟误差,而事务控制器的总体用时可能会稍微大于事务控制器下各个子项用时之和。
它有两个参数项:
建立以下结构的脚本:
【BeanShell PreProcessor】中写入以下语句,它的作用是使HTTP请求1执行前等待2000ms(BeanShell PreProcessor会在后面Beanshell专题中详细讲解)。
运行脚本,查看结果树和聚合报告:
可以看到聚合报告中记录了【事务处理器】的响应用时信息。我们勾选了【Generate parent sample】后再次运行,我们发现结果树和聚合报告都有了变化,结果树中依然能看到HTTP请求,但已经归集到事务控制器下,而聚合报告中不再显示取样器。
我们再勾选【include duration of timer and pre-post processors in generated sample】后运行脚本,区别就是聚合报告中事务控制器响应时间包含了PreProcessor的时间(2000ms)。
临界区控制器,这个名字听起来很难理解,其实这个控制器的作用是为它的子项加一个同步锁,使得在多线程场景下,同一时刻,只有一个线程能够调用其子项。我们用实际操作来验证一下它的作用。建立如下图的脚本结构:
然后设置线程组线程数为5,循环次数为2,设置固定定时器线程延迟为1000ms(固定定时器介绍见后文,这里定时器的作用是使每次HTTP请求先等待1s),而HTTP2请求是空的,目的是让HTTP请求和固定定时器的单次整体用时为1s。
运行后,观察结果树和聚合报告,可以观察到,HTTP请求是1s中被执行一次(HTTP请求是空请求,本身几乎不耗时,但由于固定定时器的存在,HTTP请求的单次用时是1s),因此Critical Section Controller的线程同步锁作用得到验证。
我们改变脚本结构,可以看到HTTP请求2同一时刻会被多个线程调用,tps也得以提升。
权重开关控制器(直译),它能分配其子项目(Child Item)的权重,从而控制子项的执行概率。首先建立如下的脚本结构:
在bzm - Weighted Switch Controller下有两个HTTP请求,将它们的Weight设置为7和3,线程组循环次数设为100,当脚本运行结束后,观察聚合报告,可以看到,HTTP请求1和HTTP请求2分别执行了70次和30次。经过多次测试,这个权重是精确控制,而非概率性控制。
Weighted Switch Controller配合其他控制器,会有更丰富的用法,比如以简单控制器、循环控制器作为子项,甚至以自身作为子项,这里不再赘述,感兴趣的朋友可以动手做下测试。