0


JMeter —— 3万字讲解让测试彻底臣服的基于 Java 之强大测试工具

一. 前言

Apache JMeter 是 Apache 组织基于 Java 开发的压力测试工具,用于对软件做压力测试。JMeter 最初被设计用于 Web 应用测试,但后来扩展到了其他测试领域,可用于测试静态和动态资源,如静态文件、Java 小服务程序、CGI 脚本、Java 对象、数据库和 FTP 服务器等等。JMeter 可对服务器、网络或对象模拟巨大的负载,在不同压力类别下测试它们的强度和分析整体性能。另外,JMeter 能够对应用程序做功能/回归测试,通过创建带有断言的脚本来验证程序是否返回了期望结果。为了最大限度的灵活性,JMeter 允许使用正则表达式创建断言。

本文基于 JMeter5.1.1 版本。

下面开始介绍 JMeter 工具及如何使用。

二. JMeter 下载与安装

2.1. JMeter 下载

JMeter 官网:https://jmeter.apache.org/

JMeter 各版本下载地址:https://archive.apache.org/dist/jmeter/binaries/

本文及后续都使用的5.1.1版本,即下图中红圈中:

2.2. JMeter 安装

JMeter 运行依赖 JDK,关于 JDK 的安装与配置请参见《JDK 环境变量设置》。

JMeter 其实是免安装的,将下载的压缩包解压,即可使用。将 jmeter.bat 创建快捷方式到桌面,就可以双击图标运行 JMeter 了。

JMeter 安装后,可能会会遇到一些异常,例如:

'findstr' 不是内部或外部命令,也不是可运行的程序或批处理文件。
Not able to find Java executable or version. Please check your Java installation
errorlevel=2
请按任意键继续…

或者是:
jmeter 安装启动报错:Not able to find Java executable or version,Please check your Java installation。

解决方案就是:环境变量 PATH 添加:%SystemRoot%/system32;%SystemRoot%。

二. TestPlan 和线程组

TestPlan(测试计划)是使用 JMeter 进行测试的起点,也是其它 JMeter 测试元件的容器。一个 JMeter 脚本有且只有一个测试计划。

线程组元件是一个测试计划的起点,测试计划的所有元件都要包含在线程组中。一个测试计划中可以有多个线程组。

线程组

  • 名称和注释:自行填写,最好具有一定意义。

线程属性

  1. 线程数:即并发用户数,需要多少用户并发,就在这里设置多少个线程数。
  2. Ramp-Up Period(in seconds):设置的虚拟用户数需要多长时间全部启动。如果设置线程数为100,准备时长为10,那么就会在10秒钟内启动100个线程。
  3. 循环次数:每个线程发送请求的次数。如果线程数为10,循环次数为100,那么每个线程发送100次请求,总请求数为10*100=1000 。如果勾选了“永远”,那么所有线程会一直发送请求,一直到主动停止运行脚本。
  4. Delay Thread creation until needed:延迟线程组创建,直到线程需要执行时。
  5. 调度器:有时候我们并不希望我们的脚本立即开始运行,而是在特定时段运行,这就需要用到调度器。调度器有持续时间&启动延迟和启动时间&结束时间两种组合。调度器要先勾选后才会生效,而且需要将循环次数设置为永远,否则当运行次数达到循环次数后,脚本会立即停止,持续时间和结束时间不再生效。
  6. 持续时间和延迟启动:持续时间和启动延迟内填入整数,表示脚本在被启动后,等待【启动延迟】内设置的秒数后,线程组内的元件才会被执行,执行时长为【持续时间】设置的秒数。如果【持续时间】和【延迟启动】做了设置的话,会优先于启动时间和结束时间生效。
  7. 启动时间和结束时间:当点击开始测试时,将等到【启动时间】填写的时间开始运行线程组下的元件,然后会在【结束时间】填写的时间点结束。

三. JMeter 接口测试

说起接口测试,首先要读懂接口文档,以最常见的 http 协议接口为例,接口文档大多是这样的:

Host(地址):http://api.test.com
Path(路径):/user/login
请求方式:POST
请求参数:
参数名称参数类型是否必填字段说明usernameString是用户名passwordString是密码deviceString否登录设备:web、Android、IOS
上面是一个登录接口文档,提供了请求地址、路径、请求方法和参数信息,那么怎么用 JMeter 按照这个接口文档调用该接口呢?请往下看。

TestPlan(测试计划)是 JMeter 所有组件的根节点,打开 JMeter 即默认有一个 TestPlan,也只能有一个 TestPlan。在 TestPlan 下添加线程组,线程组可以添加多个,线程组是相对独立的。

首先在线程组上 右键 => 取样器 => HTTP 请求,添加了一个 HTTP 请求取样器,按照接口文档填入信息:

协议:分为 http和 https(加密的 http),测试环境一般采用 http,生产环境使用 https。
服务器名称或 IP:填入 host 对应的内容(api.test.com 是域名,因为测试环境做了相关配置,否则使用 IP)。
端口号:如果服务器地址填 IP,这项就要填写相应的 IP,具体也看测试环境配置。
路径:填入接口文档的 Path 即可。

下面用 WeatherWS Web 接口实际测试一下:http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?op=getSupportCityString

接口描述是这样的:

POST /WebServices/WeatherWS.asmx/getSupportCityString HTTP/1.1
Host: ws.webxml.com.cn
Content-Type: application/x-www-form-urlencoded
Content-Length: length

theRegionCode=string

添加【察看结果树】:

运行,察看结果:

一个简单的 http 接口调用就这样完成了,但接口测试还远不止这些,比如断言(响应断言和BeanShell 断言)、关联等,这些可见下面内容。

响应乱码问题:

在 JMeter bin 目录下的 jmeter.properties 文件中,插入下图中语句并重启 JMeter 即可:

四. JMeter 断言实现

4.1. 什么是断言

什么是断言?在接口测试中,我们预设接口响应结果中会出现一个片段,我们称之为预期值,断言会在接口调用后尝试捕捉这个预期值,如果能捕捉到,则判定接口成功,否则判定接口为失败。用过 LoadRunner 的朋友一定记得检查点这个概念,断言和检查点实质上是一样的。

为什么需要断言?因为 JMeter 默认接口响应码200即为成功:

其实,接口的业务并非成功,只是接口响应是成功的:

为了甄别接口是否实现业务上的成功,我们便需要引入断言。

4.2. 断言的实现

在取样器上右键 => 添加 => 断言,可以看到,JMeter 提供了丰富的断言实现:

4.2.1. 响应断言

这大概是最常用的断言了。创建方式:在线程组或者 HTTP 请求上右键 => 添加 => 断言 => 响应断言,就创建了一个断言。

关于**断言的作用域**:在线程组上创建断言,则该断言对线程组下所有取样器生效,而在取样器上创建,则只对该取样器生效。

一般情况下,我们用默认配置即可。各个项目也简略解释一下:

1. Apply to:即应用范围,一般使用 Main sample only 模式。Main sample 代表主请求,Sub-samples 代表主请求下的子请求。

2. 测试字段(Field to test):表示在何种内容中匹配预期字段,一般选择响应文本。

  • 响应文本:接口的 Response 的全部数据。
  • 响应代码:取样器结果中的 Response Code,常见的有200(成功)、302(重定向)、404(不存在)、400(参数错误)、500(服务器错误)等。但当我们要验证404、501等 http 响应代码时,需要勾选 ignore status,因为当 http 响应代码为404、500时,JMeter 会默认这个请求是失败的。
  • 响应信息:取样器结果中的 Response Message。
  • 响应头:响应数据的Response Headers。
  • 请求头:请求的Request Headers。
  • URL 样本:有跟随重定向时,URL 样本包含了请求的 URL 和重定向的 URL,否则只包含请求 URL。

3. 模式匹配规则(Pattern Matching Rules):即响应数据与预期值的对比方式。

  • 包括(Contains):响应数据包含预期值(相当于 String 的 contains 方法),则判定接口成功,支持正则匹配。
  • 匹配(Matches):用正则表达式匹配返回结果,且必须能匹配整个返回值,而不是返回值的一部分。
  • 相等(Equals):响应数据与预期值完全相同才判定接口成功,不支持正则表达式。
  • 字符串(Substring):与【包括】类似,但不支持正则匹配。
  • 否(Not):取反,也就是前面【包括】等判定结果为 true,断言结果为 false。反之,前面判定为 false,断言结果为 true。
  • 或者(Or):勾选此项后,可以设置多个预期值,只要有一项判定成功,则断言判定成功。

4. 测试模式(Patterns to test):预期值表达式,可以是正则表达式,也可以是字符串。可以通过【添加】按钮添加一个或多个,有多个的话,不勾选【模式匹配规则】中的【或者】,则为并集模式,即全部匹配成功才断言成功。

5. 自定义失败消息(Custom failure message):当判断返回值为错误的时候,自定义的失败消息会在断言结果中显示。

实例演示:

请求百度:

响应数据:

在【百度一下】请求下添加响应断言:

执行请求,查看结果树:

结果被标红,接口判定为失败。可见,你预设的断言决定了接口是否被判定成功。把上面断言中的“百度两下”改为“百度一下”,再次执行,则接口判定为成功。

4.2.2. JSON 断言

JSON 断言针对响应结果是 applicaton/json 格式的请求进行断言。

在取样器下右键 => 添加 => 断言 => JSON 断言,就创建了一个 JSON 断言,界面如下:

  • Assert JSON PATH exists:针对响应结果中特定字段进行提取的路径表达式。
  • Additionally assert value:附加断言值,勾选此项后才能填写 Expected value。
  • Match as regular expression:作为正则表达式匹配。
  • Expected Value:预期值,这个值将与Assert JSON PATH exists提取到的值对比,一致则判定接口成功。

实例演示:

首先,创建一个 BeanShell 取样器模拟接口,返回 JSON 数据:

return "{\"code\":0,\"message\":\"操作成功\",\"data\":{\"student\":[{\"name\":\"zhangxu\",\"age\":18},{\"name\":\"zhoushuang\",\"age\":18},{\"name\":\"changhua\",\"age\":17}]}}";

在该取样器下添加 JSON 断言,照下图写入:

这里解释一下【JSON Path expression】的写法,首先$.这部分是固定写法,后面根据层级关系去逐级获取数据:

  1. $.data 表示在 JSON 串以"data"为 key 获取 value,也就是 {“student”:[{“name”:“zhangxu”,“age”:18},{“name”:“zhoushuang”,“age”:18},{“name”:“changhua”,“age”:17}]}。
  2. $.data.student 对应的是 [{“name”:“zhangxu”,“age”:18},{“name”:“zhoushuang”,“age”:18},{“name”:“changhua”,“age”:17}],因此要通过索引定位到数组中的具体对象,也就是$.data.student[0] 获取到了 {“name”:“zhangxu”,“age”:18}。
  3. $.data.student[0].name 就定位到了“zhangxu”这个值。

运行脚本,察看结果树显示接口成功。

如果我们把 Expected Value 改为"zhangxu2",再次运行,发现接口被判定失败:

4.2.3. BeanShell 断言

这种断言是应对复杂校验的利器,为什么这么说呢?因为 BeanShell 断言可以写较复杂的逻辑判断来对结果进行校验。

还以 JSON 断言 中的模拟接口为例,创建一个 BeanShell 断言:

运行脚本,察看结果树:

当然,这只是一个简单的示例,更丰富的用法下面章节。

4.2.4. 大小断言

大小断言:判断响应结果选定部分的字节大小是否与预期的值相符。

4.2.5. 断言持续时间

断言持续时间:判断接口的响应时间是否小于等于设定值。

五. JMeter BeanShell 断言

上面简单地介绍了一下 BeanShell 断言,这里作详细介绍。

5.1. BeanShell 介绍

BeanShell 是一种完全符合 Java 语法规范的脚本语言,但又拥有一些独有的语法和方法。(当然,也有一些不一样的地方,比如 BeanShell 就不支持 Java 中的泛型,当初也是一个深坑)。

BeanShell 应用在 JMeter 的四个组件中:

  1. BeanShell Sampler:BeanShell 取样器,位于 Sampler(取样器)中,与常用的【HTTP请求】取样器性质类似,也是一个独立的取样器,会被【聚合报告】所统计。
  2. BeanShell PreProcessor:BeanShell 前置处理器,位于【前置处理器】中,作用于一个取样器上,且在取样器执行前执行,一般用于对取样器入参进行处理。
  3. BeanShell PostProcessor:BeanShell 前置处理器,位于【后置处理器】中,作用于一个取样器上,且在该取样器执行后执行,一般用于对取样器结果进行处理。
  4. Beanshell Assertion:Beanshell 断言,位于【断言】中,作用于取样器上,且在该取样器执行后执行,用于对取样器响应结果进行断言。

介绍几个 BeanShell 常用的独有方法:
序号名称作用1vars.get(“variableName”)根据变量名获取内存中的变量值,前提是这个值在脚本前文中已经定义并赋值2vars.put(“variableName”,“variableValue”)将一个值存储到变量中,脚本下文中可以通过 ${variableName} 引用3prev.getResponseDataAsString()获取 sampler(取样器)的响应数据并以String类型接收,用在【后置处理器】的【BeanShell PostProcessor中】
更多内置方法见:JMeter API 文档。

5.2. BeanShell 操作变量

前面说到了 BeanShell 的独有方法,vars.get 和 vars.put 了,BeanShell 对变量的操作主要就是依赖这两个方法。

首先创建一个脚本,【用户定义的变量】中定义了一个变量,变量名为 paramIn,值为 Mu:

【BeanShell Sampler】中写入下面语句:

HTTP 请求对【BeanShell Sampler】中 put 出的变量进行引用:

运行脚本,查看结果树,可以看到变量引用成功:

在 BeanShell 中直接写代码,方便快捷,在代码量不大的情况下十分便利。如果出于规范化考虑,尤其代码量较大时,为了使 BeanShell 看起来更清晰,可以按下面的方式写,效果是一样的:

// 定义一个方法
public void test() {
    // vars.get 获取paramIn的值
    String paramIn = vars.get("paramIn");
    // 一个简单的字符串拼接
    String string = "Hello," + paramIn;
    // vars.put()生成一个变量且赋值
    vars.put("param", string);
    vars.put("paramOut", string + ",Welcome");
}

// 需要主动调用函数,否则函数不会自动起作用
test();

5.3. BeanShell 引用外部资源

如果 JMeter 脚本的代码量比较小,那么直接将代码写在 BeanShell 中就可以了。如果代码量比较大,在 BeanShell 里写起来就比较困难,这时候可以考虑引用外部资源,包括引用 .java 文件、.class 文件、.jar 文件三种方式。

5.3.1. 引用 java 文件

首先,我们写好这么一个类,类内的 md5Encryption 方法,是将一个字符串转化为一个经过 MD5 加密过的新字符串:
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class Md5Encryption {
    public static String md5Encryption(String string) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(string.getBytes());
            byte[] b = md.digest();
            int i;
            StringBuffer buf = new StringBuffer("");
            for (int offset = 0; offset < b.length; offset++) {
                i = b[offset];
                if (i < 0)
                    i += 256;
                if (i < 16)
                    buf.append("0");
                buf.append(Integer.toHexString(i));
            }
            return buf.toString();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        }
    }
}

上面的代码在 Md5Encryption.java 文件中,JMeter 支持直接引用 java 文件:

// 引用外部.java文件,注意路径中要使用"/"
source("D:/Md5Encryption.java");

String passwordIni = "123456";

// 调用加密方法
String passwordEncryp = new Md5Encryption().md5Encryption(passwordIni);
vars.put("passwordEncryp", passwordEncryp);

运行结果:

注意:如果引用的 java 文件中,又依赖了其他 jar 包,那么也需要将相应的 jar 包导入 JMeter 的安装目录的 /lib/etc 中并重启 JMeter,引用 .class 和 .jar 也是同理。

5.3.2. addClassPath

如果一个 java 文件不满足需求,那么可以把引用范围扩大到整个项目,如下,整个 mutest 项目src 目录下的所有类都可以通过 import 方式引用:

// 添加路径:类所在项目的目录
addClassPath("E:/project/workspace/mutest/src");

// 引入
import mutest.Md5Encryption;

String passwordIni = "123456";
// 调用加密方法
String passwordEncryp = new Md5Encryption().md5Encryption(passwordIni);
vars.put("passwordEncryp", passwordEncryp);

5.3.3. 引用 jar 包

前面两种方式受外部影响太大,比如 Md5Encryption 被修改了,或者路径发生变化了,JMeter脚本都会受影响。为了规避这种影响,我们可以将项目打成 jar 包,导入 JMeter 安装目录 \lib\etc中,并重启 JMeter。

上面步骤完成后,BeanShell 中直接 import 即可使用:

// import时带上类的包名
import mutest.Md5Encryption;

String passwordIni = "123456";
// 调用加密方法
String passwordEncryp = new Md5Encryption().md5Encryption(passwordIni);
vars.put("passwordEncryp", passwordEncryp);

5.4. BeanShell 断言

5.4.1. 响应断言

接口测试中,所谓断言,是指用一定的判断规则对接口响应数据进行校验,不匹配则判定接口测试失败。在 JMeter 中,不加断言的话,默认校验接口的响应码。

例如下面的例子,登录失败,但没有添加断言,且接口响应码是200,所以接口被 JMeter 判定为成功:

根据接口定义(一般由开发提供的接口文档定义),登录成功后,返回内容的 message 是“操作成功”,于是我们给接口添加一个【响应断言】:

再次运行,可以看到接口被判定为失败:

对于这种校验比较简单的接口,【响应断言】能够满足要求,但遇到结果校验很复杂的接口,【响应断言】就无法胜任了,这时候就要用到【BeanShell 断言】了。

5.4.2. 校验 JSONObject

首先使用【BeanShell Sampler】作为 mock server 返回这样的预期结果:

{
    "code": 0,
    "goodsInfo": {
        "name": "computer",
        "price": 4500,
        "size": 60
    }
}

首先,我们用 BeanShell 取样器模拟接口返回特定响应结果:

假如这个数据是根据 id=1(id 是接口的一个入参)的条件获取的,那么我们就可以编写 sql 语句去数据库查询数据了。数据库中的数据:

构造【JDBC Request】去获取数据库数据:

【BeanShell断言】,JSON 处理用得是阿里的 fastjson,jar 包自行下载后放入 JMeter 的安装目录的 /lib/etc 中并重启 JMeter:

运行后,查看结果:

附上【BeanShell 断言】代码:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
// prev.getResponseDataAsString()方法获取接口响应数据
String goodsDataRes = prev.getResponseDataAsString();
// 将String解析成JSONObject并获取goodsInfo
JSONObject goodsInfoRes = JSON.parseObject(goodsDataRes).getJSONObject("goodsInfo");
// 定义一个新的JSONObject用来存储数据库数据
JSONObject goodsInfoDb = new JSONObject();

try {
    goodsInfoDb.put("name",vars.get("name_1"));
    // 这里注意将数字转为int类型
    goodsInfoDb.put("price",Integer.valueOf(vars.get("price_1")));
    goodsInfoDb.put("size",Integer.valueOf(vars.get("size_1")));

    if(!goodsInfoRes.equals(goodsInfoDb)){
        Failure = true;
        String message = "接口返回数据与数据库数据不一致!\n";
        FailureMessage = message + "数据库内容: " + goodsInfoDb + "\n响应内容: " + goodsInfoRes;
    }
} catch(Exception e) {
    Failure = true;
    String message = "数据库数据为空!\n";
    FailureMessage = message + "数据库内容: \n" + goodsInfoDb + "\n" + "响应内容: \n" + goodsInfoRes;
}

5.4.3. 校验含 JSONArray 的 JSON

我们将问题复杂化一些,假如接口返回的是包含 JSONArray 的数据,如下:

{
    "code": 0,
    "data": [{
            "name": "iphone",
            "price": 6000,
            "size": 55
        }, {
            "name": "watch",
            "price": 500,
            "size": 35
        }, {
            "name": "computer",
            "price": 4500,
            "size": 60
        }
    ]
}
return "{\"code\" : 0,\"data\" : [{\"name\" : \"iphone\",\"price\" : 6000,\"size\" : 55}, {\"name\" : \"watch\",\"price\" : 500,\"size\" : 35},{\"name\" : \"computer\",\"price\" : 4500,\"size\" : 60}]}";

上面的数据,对应数据库里 status=1 的数据:

针对这种需求,sql 语句要修改一下:

这个接口结果校验的难点在于,接口数据与数据库数据的顺序不对应,且无法事先确定其顺序,如果直接对比两个 JSONObject,由于 JSONArray 中元素顺序不同,会导致对比返回 false,尽管两个 JSONObject 中的数据除了顺序,其他都相同。

要解决顺序问题,我想到的方案是对数据进行进一步加工,将 JSONArray 处理成 JSONObject 格式,这样就消除了顺序的影响(不含 JSONArray 的 JSONObject 的对比是不受元素顺序影响的),【BeanShell 断言】内代码贴上:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSONArray;

String goodsDataRes = prev.getResponseDataAsString();
JSONArray goodsListRes = JSON.parseObject(goodsDataRes).getJSONArray("data");
vars.put("goodsListRes",goodsListRes.toString());

JSONObject goodsInfoDb = new JSONObject();
JSONObject goodsInfoRes = new JSONObject();

try {
    for (int i = 1;i <= Integer.parseInt(vars.get("name_#")); i++) {
        JSONObject goods = new JSONObject();
        goods.put("name", vars.get("name_" + i));
        goods.put("price", Integer.parseInt(vars.get("price_" + i)));
        goods.put("size", Integer.parseInt(vars.get("size_" + i)));
        goodsInfoDb.put(vars.get("name_" + i), goods);
    }

    for (int i = 0; i < goodsListRes.size(); i++) {
        JSONObject goods = goodsListRes.getJSONObject(i);
        goodsInfoRes.put(goods.getString("name"), goods);
    }
    
    if (goodsInfoRes.size() != goodsInfoDb.size()) {
        Failure = true;
        String message = "接口返回数据与数据库数据的数量不一致!\n";
        FailureMessage = message + "数据库数据数量: " + goodsInfoDb.size() + "\n响应数据数量: " + goodsInfoRes.size() + "\n数据库内容: " + goodsInfoDb + "\n响应内容: " + goodsInfoRes;
    } else if (!goodsInfoRes.equals(goodsInfoDb)) { 
        Failure = true;
        String message = "接口返回数据与数据库数据的内容不一致!\n";
        FailureMessage = message + "数据库内容: " + goodsInfoDb + "\n响应内容: " + goodsInfoRes;
    }
} catch(Exception e) {
    Failure = true;
    String message = "数据处理异常!\n";
    FailureMessage = message + "数据库内容: \n" + goodsInfoDb + "\n" + "响应内容: \n" + goodsInfoRes;
}

使用这个断言,我们测试一下:

1. 数据处理异常,可以人为将 sql 写错,例如:select price,size from test.goods where status=1(缺少name字段):

2. 接口数据与数据库数据的数量不一致,可以将数据库数据篡改一下:

3. 数据库数据和接口响应数据数量一致,但内容不同:

4. 数据库数据与接口响应数据一致,断言通过:

六. 逻辑控制器

Jmeter 提供了多种逻辑控制器,下面将会对它们的作用和用法做详解。对于下文中多次使用的【BeanShell Sampler】,上文已经介绍其用法,这里先把它简单地看做能返回特定的值的一个请求即可。

6.1. If Controller(if 控制器)

** If 控制器**,在这个控制器中,我们可以写一些条件表达式,表达式结果为 true,则该控制器下的子项会被执行,否则不执行。

在5.1.1版本中,【Interpret Condition as Variable Expression】是默认勾选的,意为【将条件解释为变量表达式】。在这种模式下,1 != 1、1 == 1等条件表达式是会被一律判定为 false 的,这点要注意。

警告标识旁子句的意思是:为了提升性能起见,建议勾选【Interpret Condition as Variable Expression】,并使用 _jexl3 或 _groovy 去评定条件结果是 true 还是 false。

变量表达式的示例:

  1. ${__jexl3(${COUNT} < 10)}:变量表达式判断 COUNT 这个变量小于10(COUNT 是此控制器前被定义并赋值的);
  2. ${__jexl3(1 == 1)}:变量表达式判断 1==1是否成立;
  3. ${__groovy(vars.get("myVar") != "Invalid" )}:判断 myVar 不是无效的;
  4. ${__groovy(vars.get("myInt").toInteger() <=4 )} :判断 myInt 这个变量小于等于4;
  5. ${__groovy(vars.get("myMissing") != null )}:判断 myMissing 这个变量不是 null;
  6. ${RESULT}:RESULT 这个变量本身的值应该是 true 或者 false;
  7. ${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:

【If 控制器】中取消勾选【Interpret Condition as Variable Expression】,写入条件表达式:

运行脚本,查看结果树,HTTP 请求1和 HTTP 请求2都被执行。

当然,更好的选择是使用变量表达式,但前提是勾选【Interpret Condition as Variable Expression】(不勾选也能使用,但影响性能):

6.2. While Controller(判断循环控制器)

判断循环控制器,作用是循环运行其子项,直到条件为 false。这个控制器和 Java 中的 while 语法是很相似的,和【If 控制器】比起来,在 Condition 上有相似之处。

【Condition】可以填入的值有:

1. 空白:【While 控制器】下的最后一个请求(也就是图中的 HTTP请求3)失败,退出循环。

2. LAST:【While 控制器】下的最后一个请求(也就是图中的 HTTP请求4)失败,退出循环。如果在进入【While 控制器】前的最后一个 HTTP 请求失败了,那么【While Controller】将不会执行。

3. 其它:表达式结果为 false 时,退出循环。有以下情形:

  1. ${myVar} :变量 myVar 在其它项中被赋值为 false;
  2. ${__javaScript(${C}==10)}:针对数字型变量进行对比判断,这种表达式的计算结果为 false时不进入或者退出循环;
  3. ${__javaScript("${C}"=="abc")}:针对字符串类型变量进行对比判断,这种表达式的计算结果为“false”时不进入或者退出循环(区别在于双引号);
  4. ${__javaScript("${VAR2}"=="abcd")}:VAR2 在其它项中被赋值与“abcd”做比较,不相等则退出循环;
  5. ${_P(property)}:属性被其它项目赋予 false。

6.3. Simple Controller(简单控制器)

** 简单控制器**是最基本的控制器,作用是将请求分组归集在一个简单控制器中,可以理解成一个模块,使得脚本结构更清晰。对 JMeter 脚本运行没有实质上的影响。

6.4. Loop Controller(循环控制器)

循环控制器,这个控制器的作用是使其子项循环运行。

循环次数(Loop Count):在输入框中输入需要循环的次数,控制器下的子项会循环相应的次数。如果勾选了【forever】,那么控制器下的子项会一直运行。

6.5. Once Only Controller(仅一次控制器)

仅一次控制器,会使该控制器下的子项每个线程只运行一次。

在线程组下添加一个仅一次控制器,该控制器下添加一个 HTTP请求1,线程组本身添加一个 HTTP请求2,并将线程组循环次数设置为4,如下:

结果如下,符合我们的预期,仅一次控制器下的 HTTP 请求只会运行一次,无论线程组循环多少次:

那么,如果脚本多线程并发呢?我们将线程组的线程数设置为2,再次运行脚本:

可见,【仅一次控制器】线程间是隔离的,每个线程启动后,会运行一次【仅一次控制器】。

如果,【仅一次控制器】和【循环控制器】结合起来呢?我们将【循环控制器】循环次数设为3,并在其下面添加【仅一次控制器】,脚本结构见下图。运行后的结果如下:

可见,【仅一次控制器】在【循环控制器】中同样生效。

6.6. Interleave Example(交替控制器)

交替控制器,使得该控制器包含的取样器步骤在每次循环中交替执行。

例如,下面的脚本中,线程组循环次数设为3,如果没有【交替控制器】,那么 HTTP请求1和HTTP请求2将会各运行3次。

运行脚本,发现 HTTP请求1和 HTTP请求2有了交替执行的效果:

再看交替控制器下的两个参数项。

  • Ignore sub-contorller blocks:忽略子控制器模块。如果勾选此项,交替控制器将 sub-controllers 像单一请求元素一样,一次只允许一个请求/控制器。
  • Interleave across threads:勾选此项后,多线程运行脚本时,交替控制器依然生效,例如线程1执行了 HTTP 请求,那么线程2将直接执行简单控制器1。

首先验证【Ignore sub-contorller blocks】的作用,建立如下脚本,线程组循环次数设为5次:

不勾选【忽略子控制模块】,结果是这样的:

勾选【忽略子控制模块】,结果是这样的:

区别很明显,勾选了【Ignore sub-contorller blocks】后,交替控制器子控制器中的取样器一次运行只会被执行一个。

6.7. Random Controller(随机控制器)

随机控制器,当该控制器下有多个子项时,每次循环会随机执行其中一个。

建立下图的脚本结构,线程组【循环次数】设置为2。运行脚本,结果如下,脚本第一次运行执行了 HTTP请求4,第二次运行执行了 HTTP请求3,符合预期。

随机控制器有一个参数项:Ignore sub-controller block(忽略子控制器模块)。如果勾选了此项,随机控制器下的子控制器中的多个子项只会被执行一个。

修改脚本结构,线程组【循环次数】为2,运行脚本,结果两次都随机到了【简单控制器1】:

勾选【Ignore sub-controller block】后再次运行脚本,两次分别随机到了简单控制器1和2,并执行了其中一个 HTTP 请求(进入简单控制器内是按顺序执行 HTTP 请求的,即第一次进入简单控制器肯定会执行该控制器下第一个请求):

6.8. Random Order Controller(随机顺序控制器)

随机顺序控制器,与简单控制器类似,会执行它下面的每个子项,只不过执行顺序是随机的。

6.9. Throughput Controller(吞吐量控制器)

吞吐量控制器,允许用户自行调整该控制器下的子项的执行频率。

吞吐量控制器有两种模式:

1. Total Executions:当该控制下的子项被执行固定数量后,停止吞吐量控制器。例如下面这个脚本,线程组【循环次数】设为6,运行脚本。

【吞吐量控制器】下的 HTTP请求1只运行了3次:

2. Percent Executions:百分比模式,该模式使吞吐量控制器下的子项执行总循环次数的一定比例(在吞吐量中设置该比例)。例如下面的脚本:

设置线程组【循环次数】为50,运行后,查看聚合报告,吞吐量控制器下的 HTTP请求1执行了30次,也就是(50*60%)次。

6.10. Runtime Controller(运行周期控制器)

** 运行周期控制器**,顾名思义,这是一种设置运行时间的控制器,它的效果就是使该控制器下的子项运行时间为【Runtime】中的数值(单位:s)。

运行脚本,右上角显示脚本运行的时间,与预设一致。

不过,经过实测,如果线程组的循环次数勾选“永远”,则 HTTP 请求会一直运行,如果循环次数填入1,则 HTTP 请求会运行 3s,循环次数填入2的话,HTTP 请求运行 6s,因此可知,在线程组不勾选“永远”的前提下,【Runtime Controller】的运行时间为【Runtime】的值乘以线程组循环次数。

6.11. Switch Controller(开关控制器)

** 开关控制器**,通过【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},运行脚本:

6.12. ForEach Controller(遍历循环控制器)

遍历循环控制器,首先看下它的各输入项:

  • 输入变量前缀:输入遍历需要的变量的前缀,图中是 host,为什么要写 host 呢?这是因为【用户定义的变量】中变量名称是 host 为前缀的,前缀是指数字前面的内容。当然这个变量还可以来自【正则表达式提取器】、【参数化】等。
  • Start index for loop:循环开始的变量索引(行数),不填则从0开始,也就是自定义变量中的第一行。
  • End index for loop:循环结束的变量索引,不包括本身,也就是如果填入3,则执行完2就会结束。
  • 输出变量名称:将读取到的变量放入内存,变量值就是输出变量名称所定义的值(本例是test)。在后面使用这个值时,使用“${输出变量名}“格式就可以了。
  • Add “_” before number:勾选后,【用户定义的变量】的变量名要写“test_1”格式,不勾选则写成“test1”格式。

用户定义的变量中设置的变量:

HTTP 请求按下图写入,来验证 ForEach Controller 的作用:

运行脚本,发现 HTTP 请求被执行了三次(end-start 的值):

6.13. Module Controller(模块控制器)

模块控制器,可以理解为对封装好的模块的调用。

观察上图的脚本结构并运行,查看结果树,可以看到,线程组1中的模块控制器可以调用线程组2中的简单控制器3及其下面的 sampler。

由此可知,模块控制器的作用在于,当一个测试片段(通常是一个包含 sampler 的控制器)在脚本中多处运行时,模块控制器可以非常便利地完成调用,避免重写这个测试片段,使脚本减少冗余,结构简洁。

另外,当测试计划中有多个线程组时,一个线程组需要运行其它线程组的一个测试片段,模块控制器的作用就更加明显了。在这种场景下,即使其它线程组被禁用,依然不影响模块控制器对其节点下测试片段的调用。而在实际测试工作中,通常是一个线程组启用,而其它线程组被禁用,防止线程组互相干扰。

使用模块控制器时,需要注意的是,要保证控制器的名字各不相同,因为模块控制器是通过控制器名去调用的。

6.14. Include Controller(包含控制器)

包含控制器,它的作用是引入外部的 jmx 文件。需要注意的有以下几点:

  1. 引用的 jmx 文件中,不能包含线程组。
  2. 当使用包含控制器中包含相同的 JMX 文件时,要避免同名。

6.15. Transaction controller(事务控制器)

** 事务控制器**,生成一个额外的采样器来测量其下测试元素的总体时间;值得注意的是,这个时间包含该控制器范围内的所有处理时间,而不仅仅是采样器的。由于时钟误差,事务控制器的总体用时可能会稍微大于事务控制器下各个子项用时之和。

它有两个参数项:

  1. Generate parent sample:生成父样本(不同的模式选择);
  2. include duration of timer and pre-post processors in generated sample:是否包含时间的计时器和前后处理器耗用的时间。

建立以下结构的脚本:

【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)。

6.16. Critical Section Controller(临界区控制器)

**临界区控制器**,这个名字听起来很难理解,其实这个控制器的作用是为它的子项加一个同步锁,使得在多线程场景下,同一时刻,只有一个线程能够调用其子项。我们用实际操作来验证一下它的作用。建立如下图的脚本结构:

然后设置线程组线程数为5,循环次数为2,设置固定定时器线程延迟为 1000ms(固定定时器介绍见后文,这里定时器的作用是使每次 HTTP 请求先等待 1s),而 HTTP请求2 是空的,目的是让HTTP 请求和固定定时器的单次整体用时为 1s。

运行后,观察结果树和聚合报告,可以观察到,HTTP 请求是 1s 中被执行一次(HTTP 请求是空请求,本身几乎不耗时,但由于固定定时器的存在,HTTP 请求的单次用时是 1s),因此 Critical Section Controller 的线程同步锁作用得到验证。

我们改变脚本结构,可以看到 HTTP请求2 同一时刻会被多个线程调用,tps 也得以提升。

6.17. bzm - Weighted Switch Controller(权重开关控制器)

权重开关控制器(直译),它能分配其子项目(Child Item)的权重,从而控制子项的执行概率。

这个控制器需要安装插件,下载地址:https://jmeter-plugins.org/ ,搜索 Weighted Switch Controller 即可。

首先建立如下的脚本结构:

在 bzm - Weighted Switch Controller 下有两个 HTTP 请求,将它们的 Weight 设置为7和3,线程组循环次数设为100,当脚本运行结束后,观察聚合报告,可以看到,HTTP请求1 和 HTTP请求2 分别执行了70次和30次。经过多次测试,这个权重是精确控制,而非概率性控制:

Weighted Switch Controller 配合其他控制器,会有更丰富的用法,比如以简单控制器、循环控制器作为子项,甚至以自身作为子项,这里不再赘述,感兴趣的朋友可以动手做下测试。

七. JMeter 配置元件

JMeter 提供了丰富的配置元件,常用的包括参数化配置元件、HTTP 请求默认值、HTTP 信息头管理器、计数器等,这些配置元件用于设置默认值和变量,提供给后面的 sampler(取样器)使用。

7.1. CSV Data Set Config(参数化)

参数化配置元件(以下简称 CSV)能够在文件中读取一行数据,根据特定的符号切割成一个或多个变量放入内存中。相比于 JMeter 函数助手中提供的 __CSVRead()、__StringFromFile() 函数,CSV 使用更加简便。而且,CSV 非常适合处理大量的数据,也适用于生成“随机值”、“唯一值”这种变量。

JMeter 支持数据被双引号括起,被双引号括起的数据允许包含分隔符,例如:a,b,"c,d",这行数据被逗号分隔后将产生三个变量值:a、b、c,d。

JMeter支持读取具有标题行(标题行的内容是列名称)的 CSV 文件,要启用此功能,要将【Variable Names】留空,并且提供正确的分隔符。

参数详解
参数描述必须Name脚本中显示的这个元件的描述性名称是Filename待读取文件的名称。可以写入绝对路径,也可以写入相对路径(相对于bin目录),如果直接写文件名,则该文件要放在bin目录中。对于分布式测试,主机和远程机中相应目录下应该有相同的CSV文件是File Encoding文件读取时的编码格式,不填则使用操作系统的编码格式否Ignore first line是否忽略首行,如果csv文件中没有表头,则选择false是Variable Names变量名列表,多个变量名之间必须用分隔符分隔。如果该项为空,则文件首行会被读取并解析为列名列表否Delimiter参数分隔符,将一行数据分隔成多个变量,默认为逗号,也可以使用“\t”。如果一行数据分隔后的值比Vairable Names中定义的变量少,这些变量将保留以前的值(如果有值的话)是Allow quoted data?是否允许变量使用双引号,允许的话,变量将可以括在双引号内,并且这些变量名可以包含分隔符否Recycle on EOF?是否循环读取csv文件内容,默认为 true是Stop thread on EOF?是否循环读取csv文件内容,默认为 true是Recycle on EOF?当Recycle on EOF为false时,停止线程,当Recycle on EOF为true时,此项无意义,默认为 false是Sharing mode1、All threads(默认):一个线程组内,各个线程(用户)唯一顺序取值;2、current thread:一个线程组内,各个线程(用户)各自顺序取值;3、线程组各自独立,但每个线程组内各个线程(用户)唯一顺序取值;是
示例,注意【Sharing mode】为 All threads:

file.txt 中内容:

var1    var2    var3
a1      b1      c1
a2      b2      c2
a3      b3      c3

添加【Debug Sampler】后,设置线程组线程数为2,循环次数为2,运行脚本:

查看结果树,4次运行结果依次是:

var1=a1 var2=b1 var3=c1
var1=a2 var2=b2 var3=c2
var1=a3 var2=b3 var3=c3
var1=a1 var2=b1 var3=c1

修改【Sharing mode】为【Current thread group】后,运行结果不变,改为【Current thread】,结果变为:

var1=a1 var2=b1 var3=c1
var1=a2 var2=b2 var3=c2
var1=a1 var2=b1 var3=c1
var1=a2 var2=b2 var3=c2

以上结果说明,前两种模式下,多个线程会互相影响,线程1 读取第一行数据后,线程2 就会读取第二行,线程1 再次读取时会读取第三行,以此类推。第三种模式下,各个线程互不影响,只按自身的顺序去读取文件,例如线程1 读取第一行后,下次会读取第二行,线程2 也是如此。

7.2. HTTP Cookie Manager(Cookie 管理器)

Cookie 管理器有两个功能:
1. 它像 Web 浏览器一样存储和发送 cookie。如果有一个 HTTP 请求,并且响应中包含一个cookie,那么 Cookie 管理器会自动存储该 cookie,并将其用于将来对该特定网站的所有请求。每个 JMeter 线程都有自己的“cookie 存储区”。因此,测试使用 cookie 存储会话信息的网站,每个JMeter 线程都将有自己的会话。要注意的是,此类 cookie 不会出现在 Cookie 管理器显示中,但可以在【查看结果树】中查看它们。

JMeter 会检查接收到的 cookie 是否对 URL 有效,这意味着跨域 cookie 将不会被存储。如果想要进行窃听行为或希望使用跨域 cookie,需要定义 JMeter 属性“CookieManager.check.cookies=false”。

接收到的 cookie 可以存储为 JMeter 线程变量。要将 cookies 保存为变量,请定义属性“CookieManager.save.cookies=false=true”。此外,在存储 cookie 名称之前,它们的前缀是“COOKIE_”(这可以避免局部变量的意外损坏)要恢复到初始状态,请定义属性“CookieManager.name.prefix=”(一个或多个空格)。如果启用,名为 TEST 的 cookie 的值可以描述为 ${COOKIE_TEST}。

2. 我们可以手动将 cookie 添加到 Cookie 管理器中。不过,这样做的话,所有 JMeter 线程将共享 cookie。这些 cookie 的失效时间将会很长。默认情况下,空值的 cookie 将会被忽略,可以通过设置 JMeter 配置文件来修改:CookieManager.delete_null_cookies=false,这也适用于手动定义的 cookie,任何此类 cookie 在更新时都将从显示中被删除。cookie 名称必须是唯一的,如果用相同的名称定义了第二个 cookie,它将替换第一个。

注:如果一个 sampler(取样器)的作用域中有多个 Cookie 管理器,则无法指定要使用哪个Cookie 管理器。另外,存储在一个 Cookie 管理器中的 cookie 对任何其他管理器都不可用,因此请小心使用多个 Cookie 管理器。

Cookie 管理器参数详解:
名称描述必须Name脚本中显示的这个元件的描述性名称否Clear Cookies each Iteration勾选此项后,每次执行主线程组循环时,都会清除所有服务器定义的cookie。在GUI中定义的任何cookie都不会被清除。否Cookie Policycookie管理策略。“standard”适用于大部分情况。“ignoreCookies”等同于忽略cookieManger是User-Defined Cookies使用自定义的cookie否

7.3. HTTP Request Defaults

**HTTP 请求默认值**,这个元件可以设置 HTTP 请求控制器使用的默认值。例如,图中【服务器名称或 IP】项目内填入了【example.com】,后面的 HTTP 请求如果 IP 也是 example.com 的话,那么只要将【服务器名称或 IP】留空,那么这个字段将自动继承 HTTP 请求默认值中的值。其他诸如协议、端口号、路径等同此。

HTTP 请求默认值可以极大地增强复用性,简化脚本编写:

7.4. HTTP Header Manager(信息头管理器)

** 信息头管理器**,支持用户添加或者重写 HTTP 请求头。JMeter 支持多个信息头管理器。多个信息头条目合并成一个信息头列表,跟随 http 请求一并提交到服务端。

经过实测,总结出规则如下:

  1. 当有多个信息头管理器,且不同的管理器内有名称相同的信息头条目存在时,顺序靠前的管理器的信息头条目会覆盖后面的:

例如上图,运行后,查看结果树:

  1. 当只有一个信息头管理器,但管理器内有名称相同的信息头条目时,会同时生效;将上面脚本中的 HTTP 信息头管理器1 禁用,再次运行脚本,查看结果树:

7.5. Counter(计数器)

** 计数器**,可以在线程组任何位置创建,允许用户配置起点、最大值和增量。配置后,计数器将从起点循环到最大值,然后重新开始,直到线程结束。

计数器参数详解:
名称描述必须Name脚本中显示的这个元件的描述性名称是Starting value计数器的起始值,默认为0否Increment每次迭代计数器的增量,默认为0,表示无增量否Maximum value计数器最大值,到此值后将从起始值重新开始计数,默认为LONG.MAX_VALUE否Number format可选格式,例如000将格式化为001、002等。这将传递给decimalFormat,因此可以使用任何有效格式。如果在解释格式时出现问题,则忽略该格式。【默认格式是使用 long.toString() 生成的】否Exported Variable Name引用名称,如果将它命名为count,后面可以用${count}引用它,不建议用空字符串,虽然空值也可以使用否Track Counter Independently for each User如果勾选此项,则计数器为全局计数器(即,用户1将获得值“1”,用户2将在第一次迭代时获得值“2”)。如果选中,则每个用户都有一个独立的计数器。否Reset counter on each Thread Group Iteration此选项仅在勾选【Track Counter Independently for each User】时可用,如果选中此选项,则计数器将重置为每个线程组迭代的起始值。当计数器在循环控制器中时,这可能很有用。否
实际工作中,计数器一般和逻辑控制器配合使用,例如下面的脚本:

运行脚本,查看结果树。分析一下这个运行结果:

  1. 第1次循环,count=1,不满足 if 控制器1 条件,满足 if 控制器2 条件,执行了 控制器2 下的取样器;
  2. 第2次循环,count=2,不满足 if 控制器1 条件,满足 if 控制器2 条件,执行了 控制器2 下的取样器;
  3. 第3次循环,count=3,满足 if 控制器1 条件,也满足 if 控制器2 条件,执行了 控制器1 和控制器2 下的取样器;
  4. 第4次循环,count=4,不满足 if 控制器1 条件,满足 if 控制器2 条件,执行了 控制器2 下的取样器;
  5. 第3次循环,count=5,不满足 if 控制器1 条件,也不满足 if 控制器2 条件,两个 if 控制器 下的取样器都没有执行。

八. JMeter 操作 MySql 数据库

JMeter 连接 MySql 数据库是很方便的,下面就演示一下具体的操作。

首先,在本地安装好 MySql 服务器,建立库和表,并准备数据以供测试使用。

接下来,在【测试计划】中添加 JDBC 驱动,即下载 mysql-connector-bin.jar 并添加到 classpath中。如下图所示:

当然,如果将这个 jar 包直接放在 JMeter 安装目录的 lib 文件夹中,就无需在【测试计划】中添加了。

当上面的准备工作完成后,就可以在线程组下添加一个【配置元件】=>【JDBC Connection Configuration】:

参数详解:
名称描 述必须Name脚本中显示的这个元件的描述性名称是Variable Name Bound to Pool连接所绑定的变量名。可以创建多个连接,每个连接绑定到不同的变量名,后续【JDBC Request】通过对应variable name使用相应的连接是Max Number of Connections连接池最大允许连接数。默认设置为0,代表每个线程获得自己的连接池。如果使用共享连接池,将其设置成与线程数相同即可是Max Wait (ms)超时时间。如果尝试连接的过程超过了这个时间,则抛出异常并停止连接是Time Between Eviction Runs (ms)运行状态下,空闲对象回收线程休眠时间。如果设为负数,空闲对象回收线程将不会运行是Auto Commit自动提交开关,true代表开启是Transaction Isolation事务隔离,一般使用默认即可是Test While Idle当连接空闲时是否进行测试是Soft Min Evictable Idle Time(ms)连接可以在池中处于空闲状态的最短时间,超过这个时间的空闲连接才会被回收是Validation Query用于确定数据库是否仍在响应的简单查询否Database URL数据库的JDBC连接串是Username数据库连接的用户名是Password数据库连接的密码是
添加【JDBC Request】:

最终脚本结构完成,运行脚本,结果如下:

再看 Debug Sampler 中:

可以看到,sql 查询结果存储在变量中,例如 name 的变量,name_#=3,表示 sql 语句 name 字段有3个结果,name_1、name_2、name_3,在脚本后续想使用这些值时用 ${variable name} 引用,例如用 ${name_1} 即可使用"Tom"这个值。

以上便是 JMeter 操作 MySql 数据库的常规用法,已经能满足大部分使用情况,毕竟测试中一般只会进行查库操作,很少写库。但有时候我们难免遇到一次性执行多种类型 sql 语句(类似事务)的场景,按照上文中的写法,是实现不了的。

要实现这个功能,有两种方案,如下:

方案一:

  1. 【JDBC Connection Configuration】配置页面,Database URL:jdbc:mysql://localhost:3306/hkkTest 后添加 ?allowMultiQueries=true

  1. 【JDBC Request】的【Query Type】选择 Callable Statement,然后将多条 sql 语句按顺序写好,每条语句要以分号结尾:

运行后,查看结果:

方案二:

方案二使用的前提是,MySql 服务器上已经建好相应的存储过程并执行,如下例:

delimiter $$;
# 存储过程后面必须有()
CREATE PROCEDURE user_procedure()
BEGIN
  UPDATE test.user SET age=24 WHERE name='Tom';
  INSERT INTO test.user(id,name,age,address) VALUES(4,'Lucy',17,'Paris');
  INSERT INTO test.user(id,name,age,address) VALUES(5,'Messi',32,'Barcelona');
  DELETE FROM test.user WHERE name='Jack';
  SELECT * FROM test.user;
END
$$;
delimiter;

然后,【JDBC】中【Query Type】同样选择 Callable Statement,sql 语句只写上调用存储过程的语句即可:

运行脚本,查看结果,可以看到存储过程已经被执行,数据库数据被修改成功:

九. JMeter 自定义日志与日志分析

9.1. JMeter 日志概览

JMeter 与Java 程序一样,会记录事件日志,日志文件保存在 bin 目录中,名称为 jmeter.log。当然,我们也可以在面板中直接察看日志,点击右上角黄色标志物可以打开日志面板,再次点击收起。

可见,通过日志可以帮助我们定位一些不容易直接察觉的问题。

另外,JMeter 可以很方便地设置日志输出级别:

9.2. JMeter 自定义日志

前面所看到的都是系统日志,也就是 JMeter 本身所打印的日志。如果我们自己想输出一些日志,该怎么办呢?这个一般就要借助 BeanShell 了。

例如,一个接口响应结果如下:

在该请求下添加 BeanShell 断言,运行后,日志中输出了相应内容:

import org.apache.log4j.Logger;

// 获取接口的响应数据
String result = prev.getResponseDataAsString();

if (result.contains("error")) {
    Failure = true;
    log.error("接口失败: " + result);
}

当然,自定义日志最重要的作用还是在 Linux 服务器上运行脚本时,因为没有界面,排查问题更加麻烦。

将 JMeter 脚本部署到 Linux 服务器上进行压力测试,存在一些不便之处:

  • 吞吐量统计中包括了所有请求,包括一些辅助请求(BeanShell 请求),导致真正的 tps 统计数据不准确。
  • 业务是否成功,以及具体失败原因难以排查。

首先,测试接口的响应内容如图所示:

在接口下添加2个【JSON Path Extractor】,分别用于在测试接口的响应内容里提取 code、orderId:

再在测试接口下添加【BeanShell 断言】:

import org.apache.log4j.Logger;

// 获取接口的响应数据
String result = prev.getResponseDataAsString();
// 从JSON提取器中获取code和orderId
String code = vars.get("code");
String orderId = vars.get("orderId");

if (code.equals("0")) {
    log.info("place order success, orderId=" + orderId);
} else {
    Failure = true;
    log.error("FailureMessage: " + result);
}

将该脚本上传到 Linux 中,顺便写个启动脚本 start.sh:

#!/bin/bash
jmeter_log=/home/test/jmeter.log

if [ -f "$jmeter_log" ]; then
 // 将原日志文件备份后删除
 cp $jmeter_log /home/test/jmeter.log_back
 rm -rf $jmeter_log
fi
// 启动JMeter脚本
jmeter -n -t /home/test/test.jmx -l /home/test/result/test.jtl

运行脚本后,cat jmeter.log,效果如下:

9.3. JMeter 日志分析

针对该日志写一个日志分析脚本 logAnalysis.sh:

#!/bin/bash
jmeter_log=/home/test/jmeter.log
thread_num=`grep 'Thread started' $jmeter_log|tail -n 1|awk -F"-" '{print$6}'`
start_time=`grep 'All thread groups have been started' $jmeter_log|awk -F" " '{print $1,$2}'|awk -F"," '{print $1}'`
end_time=`grep 'Shutdown hook ended' $jmeter_log|awk -F" " '{print $1,$2}'|awk -F"," '{print $1}'`

final_success_time=`grep "place order success" $jmeter_log|tail -n 1|awk -F" " '{print$1,$2}'|awk -F"," '{print$1}'`
success_running_time=$[ $(date -d "$final_success_time" +%s) - $(date -d "$start_time" +%s) ]
running_time=$[ $(date -d "$end_time" +%s) - $(date -d "$start_time" +%s) ]
cancle_times=`grep "cancle orders success" $jmeter_log|wc -l`   //撤单次数

success_times=`grep success $jmeter_log|wc -l`   // 成功次数
failure_times=`grep FailureMessage $jmeter_log|wc -l`
request_times=$[ $success_times+$failure_times ]
error_rate=`echo "scale=2; $failure_times/$request_times*100" | bc`
qps=$[ $request_times/$running_time ]
throughput=$[ $success_times/$success_running_time ]

echo -e '线程数:'$thread_num
echo -e '请求次数:' $request_times
echo -e '成功次数:' $success_times
echo -e '失败次数:' $failure_times
echo -e '撤单次数:'$cancle_times
echo -e '错误率:' $error_rate'%'
echo -e '开始时间:'$start_time
echo -e '结束时间:'$end_time
echo -e '最后成功请求时间:'$final_success_time
echo -e '请求时间:' $running_time 
echo -e '成功运行时间:'$success_running_time
echo -e '吞吐量:'$throughput'/s'
echo -e 'QPS:'$qps'/s'

当 JMeter 脚本运行一段时间后,执行 logAnalysis.sh,效果如下:

线程数:180
请求次数: 131691
成功次数: 131493
失败次数: 198
撤单次数:141
错误率: 0%
开始时间:2018-11-28 15:34:54
结束时间:2018-11-28 15:37:17
最后成功请求时间:2018-11-28 15:37:17
请求时间: 143
成功运行时间:143
吞吐量:919/s
QPS:920/s

可以看到,输出信息全面清晰。这样,我们就可以在 linux 下运行 JMeter 压测脚本时,实时获取压测详情了。

十. JMeter Linux 下执行测试

JMeter 不仅能十分便捷地进行接口测试,同时它也是一款优秀的压测工具。但使用 JMeter 在自己的电脑(下称本机)上运行压测脚本时,一般会有两个瓶颈:
  1. 网络:本机与服务器之间的网络开销,会严重影响服务的性能表现。而且,本机与服务器一般不在同一个网段,网络瓶颈会更加明显。
  2. 本机性能:工作所用的笔记本,其性能很难与 linux 系统服务器的性能相比。而且,在Windows 端运行的 JMeter 相比于 Linux 端的 no-gui 模式,本身性能也会差一些。另外,压测对本机的占用,会影响我们其他工作的开展。

基于以上两点考虑,将压测脚本部署到 Linux 服务器上运行,是一个很好的选择。在实际的性能测试中,一般会专门申请一台 CPU、内存、磁盘指标都较好的 linux 服务器作为压测机器。

10.1. JMeter 安装

版本情况:Linux 版本为 Centos7,JMeter 使用版本为3.3,JDK 版本为1.8。

JMeter 在 linux 系统中的安装十分简单:

  1. 将 JMeter 安装包通过 xshell 导入 /usr/local 目录;
  2. 将 JMeter 安装包解压缩:tar –zxvf apache-jmeter-3.3.tgz;
  3. 赋予权限:chmod -R 777 apache-jmeter-3.3;
  4. 配置环境变量:vi /etc/profile,在末尾输入:export PATH=/usr/local/apache-jmeter-3.3/bin/:$PATH,保存退出;
  5. source /etc/profile 使配置生效;
  6. 检验 JMeter 是否可以运行:jmeter –v,出现以下信息,说明 JMeter 安装配置成功。

10.2. no-gui 模式运行脚本

操作步骤:

  1. 在 Windows 下 JMeter 里编写完成脚本,脚本名称:test.jmx;
  2. 将脚本上传到 Linux 系统指定目录中,例如:/opt/local/jmeterScripts;
  3. 执行以下命令:jmeter -n -t /usr/local/jmeterScripts/test.jmx -l /usr/local/testResult/test.jtl。

-n 表示以 nogui 方式运行测试计划。
-t 表示测试计划,后面跟测试计划名称。
-l 表示测试结果,后面跟测试结果文件名称。

当控制台出现上图,说明脚本成功运行。

出于方便考虑,将启动命令写入脚本 start.sh 中:

#!/bin/bash
jmeter -n -t /usr/local/jmeterScripts/test.jmx -l /usr/local/testResult/test.jtl

脚本运行结束后,会在 testResult 目录下生成一个 test.jtl 文件,将这个文件导出到 Windows 端JMeter 中的聚合报告中,即可查看测试详情:

注意事项:

如果使用参数化策略,将参数化文件放入 Linux 系统中 JMeter 安装文件的 bin 目录下,同时脚本参数化设置中,文件路径只写文件名即可。

以往这种方法是正常的,但在最近的实践中,遇到了诡异的问题,参数化文件被报不存在的错误:

解决方案是:在 Filename 处填写参数化文件在 Linux 系统下的绝对路径,当然这个输入框是不允许输入 Linux 路径格式的,需要通过引用变量的方式赋值。

用户定义的变量中填写参数化文件的完整路径:

其他可能遇到的问题就是,脚本中引用了一些 jar 包或者使用了插件,而 linux 中的 JMeter 安装包中未含有这些资源,最简单的解决方案就是将包含完整资源的 Windows 端 JMeter 安装包压缩后上传到 Linux 系统中。


本文转载自: https://blog.csdn.net/mrluo735/article/details/135449320
版权归原作者 流华追梦 所有, 如有侵权,请联系我们删除。

“JMeter —— 3万字讲解让测试彻底臣服的基于 Java 之强大测试工具”的评论:

还没有评论