电商项目 Jmeter 脚本实战开发

1,709 阅读12分钟

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战

一、前置工作

1、黄金流程

在做性能脚本之前,先了解下这本次性能实战业务,简要说明本次使用一个电商系统的下单流程做这本次性能业务场景,该流程也叫叫黄金流程,用户从浏览首页到选择商品、加入购物车、支付等一系列步骤组合成该流程,下图是这次性能实战的业务流程图。

图片

2、Jmeter安装

脚本开发前置条件是需要在安装 Jmeter,如果没有安装的话请点击下载到官方网站下载 Jmeter; 因为 Jmete r是纯 java 开发出来的所以需要安装 jdk 环境,请大家到网上下载 jdk1.8 以上版本并且安装,在这里就不在演示安装步骤,为了方便大家配置 jdk 与 Jmeter 环境变量,这里提供 mac 电脑环境中的 jdk 与 Jmeter 环境变量,环境变量参考如下:

Jdk 环境变量参考如下:

$ vim .bash_profile

export JAVA_HOME=/usr/local/java/jdk1.8.0_181
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
export PATH=${JAVA_HOME}/bin:$PATH
#按esc键,再输入如下命令
$ :wq!
#终端输入生效命令
$ source ~.bash_profile

Jmeter 环境变量参考如下:

$ cd ~
$ vi ~.bash_profile
#Jmeter:路径
Jmeter_HOME=/Users/tools/apache-Jmeter-5.3
PATH=$PATH:$HOME/bin:$Jmeter_HOME/bin:
$ export PATH
执行生效:
$ source ~.bash_profile

验证 Jmeter 环境是否配置成功请输入【Jmeter -v】命令,如果提示如下表示配置成功,配置环境变量的好处是在终端任何文件目录下可以打开 Jmeter,如果不配置只能在 Jmeter 中的 {Jmeter_path}/bin/Jmeter 下执行:

图片

输入命令 Jmeter 命令即可启动:

liwen ~ % Jmeter
================================================================================
Don't use GUI mode for load testing !, only for Test creation and Test debugging.
For load testing, use CLI Mode (was NON GUI):
   Jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder]
& increase Java Heap to meet your test requirements:
   Modify current env variable HEAP="-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m" in the Jmeter batch file
Check : https://Jmeter.apache.org/usermanual/best-practices.html
================================================================================

下图是启动 Jmeter 后效果图:

图片

二、脚本实战开发

在开发脚本之前,先拆分两部分脚本来开发,一个部分注册流程脚本开发,一个部分是下单流程脚本开发,如下图:

图片

1、用户注册链路

为什么先从注册脚本开始因为下单流程需要用户登陆态才可以下单,所以先从注册流程中的注册脚本开始开发;

先从注册接口文档观察出来,该系统注册需要先获取手机验证码,才能调用注册接口注册到该系统中。

1.1、接口说明

注册用户接口图: 图片

为了快速开发注册脚本,先手动输入一个手机号,并且点击获取验证码,获取验证码接口如下:

图片

点击注册用户接口输入验证码、密码、手机号、用户名点击发送即可注册成功,接口文档显示如下:

图片

通过上面注册成功的用户,打开登陆接口文档,输入用户名与秘密即可登陆系统,下图是用户登陆接口登陆后响应的信息图,里面包含用户的 token,响应状态码等信息,其他需要登陆态的接口可以通过添加 token 头信息就可以获取该接口响应的信息,但是注册后的用户需添加用户地址,才能完成下单流程,如果没有用户地址,下单 是不会成功,因为下单都不知道货物送到什么地方。

图片

下图是添加用户地址信息接口,接口入参如下图:

图片

点击调试按钮,接口请求模版信息如下显示,做脚本可以参考接口文档提示而且通过提示信息就能很快把脚本开发出来。

图片

通过手动接口文档注册用户,不方便未来批量注册用户,所以需要给它们之间加上关联,只这样才能做到批量注册用户,但是如果我们压测需要大量用户,可以通过 java 或 python、excl 等工具生成一批用户并且保存到文件中,再通过 Jmeter 中的 CSV Data Set Config 组件读取文件方式获取用户参数,这样才能批量注册用户,如何批造出手机号,这里提供两个代码可以帮助大家快速生成用户手机号。

生成手机号码需要满足手机号规则,通过调研需要满足下面条件:

  • 中国电信:133、149、153、173、177、180、181、189、191、199
  • 中国联通:130、131、132、145、155、156、166、171、175、176、185、186
  • 中国移动:134(0-8)、135、136、137、138、139、147、150、151、152、157、158、159、172、178、182、183、184、187、188、198

python生成手机号码代码参考如下:

import  random
# 所有的头信息添加上来
phon = [133, 149, 153, 173, 177, 180, 181, 189, 191, 199, 130, 131, 132, 145, 155, 156, 166, 171, 175, 176, 185,
        186, 135, 136, 137, 138, 139, 147, 150, 151, 152, 157, 158, 159, 172, 178, 182, 183, 184, 187, 188, 198]
# 打开文件,并且存放到一个内存地址中
f = open("phone.txt", "w")
#循环100万次
for i in range(1, 1000000):
    # 拼接手机号码
    phone = str(random.choice(phon)) + "".join(random.choice("0123456789") for i in range(8))
    # 保存手机号
    f.write(phone)
    f.write("\n")
# 关闭文件
f.close()

java 生成手机号码代码参考:

public static void main(String[] args) throws IOException {
//保存到文件中
FileWriter fileWriter = new FileWriter("./7d/phone.txt");
//保存1000个手机号
for (int j = 0; j < 100000; j++) {
    //保存手机号数据
    String[] phone = {"133", "149", "153", "173", "177", "180", "181", "189", "191", "199", "130", "131", "132", "145", "155", "156", "166", "171", "175", "176", "185", "186", "135", "136", "137", "138", "139", "147", "150", "151", "152", "157", "158", "159", "172", "178", "182", "183", "184", "187", "188", "198"};
    Random random = new Random();
    int first = random.nextInt(phone.length);
    StringBuilder stringBuilder = new StringBuilder(phone[first]);
    for (int i = 0; i < 8; i++) {
        stringBuilder.append(random.nextInt(10));
    }
    fileWriter.write(stringBuilder.toString());
    fileWriter.write("\n");
}
//关闭文件流
fileWriter.close();
}

注意:

为什么需要用随机数做用户手机号码,而不是顺序执行生成手机号,因为顺序生成手机号码并且注册系统中最后存到数据库中这样的数据不真实,压测参数化的数据需要符合真实用户数据。

1.2、脚本开发

打开Jmeter,新建线程组,如图: 图片

通过手机号码获取验证码写法操作步骤如下:

1、新建HTTP Reques并且按如下把参数写进入即可把脚本开发出来;

2、中间 ${telephone} 是引用全局文件参数文件,后面会说到;

图片

因为用户注册需要用到手机验证码,所以需要通过关联把参数获取出来,并且给注册接口使用,如果大家不了解什么是关联请参考《性能测试实战30讲》【关联和断言:一动一静,核心都是在取数据】在这里就不详细说明,获取验证码写法参考如图: 图片

注册接口脚本第一步新建 HTTP Request 输入注册地址,并且输入相关信息,参考如下:

url:

http://cloud-gateway.mall.demo.7d.com/mall-portal/sso/register

具体写法如下: 图片

说明:

中间的 value 地方显示是的 ${username} 是引用 Jmeter 中的全局变量,这样写的好处是一个地方修改全部引用都生效,方便参数维护;

以下是全局参数文件:

图片

通过上面操作即可把用户注册完成,但是完成注册对于下单流程还是不能下单成功,因为缺少用户地址,下面再把用户地址添加到该用户,添加用户地址接口实现参考具体如下:

图片

整个注册流程 Jmeter 脚本如下显示:

图片

以上脚本是用户注册到登陆再到增加用户地址信息在 Jmeter 中实现,上面脚本中没有添加断言是因为这些脚本主要完成的事情是用户注册,用户注册信息是为下单流程做数据准备所以没添加断言,不过大家可以添加断言组件,判断请求是否正确。

注意:

如果只测下单流程而不想使用登录接口,那么只需要把用户信息与token信息保存到本地,通过参数化技术把token与用户信息读取传给需要登陆态的接口,这样才能正常压测业务场景,以下介绍怎么通过Jmeter 把数据保存到本地。

第一步添加线程组,添加 HTTP Request,输入登陆系统接口地址与用户参数信息如下:

图片

在全局参数文件中添加输出,保存文件信息路径,如图:

图片

在 Jmeter 中添加 JSR233 Sampler 组件如图:

图片

参考代码:

FileOutputStream fps=new FileOutputStream("${outfile_online}",true);
OutputStreamWriter osw=new OutputStreamWriter(fps);
BufferedWriter bw=new BufferedWriter(osw);
bw.append("${Bearer}\t${token}\n");
if(bw!=null){bw.close();}
if(osw!=null){osw.close();}
if(fps!=null){fps.close();}

通过代码编写,再点击运行验证输出结果如下:

图片

参数文件写入成功

图片

2、用户下单链路

在开发下单脚本之前,先回顾下,下单流程需要那几步骤,这样才能把脚本开发好,进而为基准测试与容量测试做准备;

图片

新建全局变量,全局变量的目的是为了更好切换压测环境与变量值,还有更好为参数化做准备,所以采用全局定义变量,操作步骤如:

1、启动 Jmeter,选择右键选择 User Defined Variables 组建即可看到相应的值,建议在每个定义变量后写上注解,这样方便记忆这个变量是什么值。

图片

下图 name 这个模块,是需参数化的的变量名,在 Value 这个模块中输入变量的值,这也是常见的 key-value 输入样式模式。

图片

在做写接口的时候先了解下接口请求类型这样方便使用什么类型做接口脚本,通常情况下请求分为 GET/POST /DELETE/PUT 等常见请求;一般 GET 请求是或者资源数据,POST 请求是提交数据,DELETE 请求是删除数据,PUT 请求是修改数据,但这些并不是唯一这样定义,举简单例子如下:

@GetMapping("/7d/test/{info}")
public MsgResponse getRequest(@PathVariable Integer info) {
    if (info == 1) {
        //更新数据状态
        return MsgResponse.success().add("data", "更新状态成功");
    } else {
        //查询数据
        HashMap<String, String> map = new HashMap<>();
        map.put("7d", "性能实战1");
        map.put("7dTest", "性能实战2");
        return MsgResponse.success().add("data", map);
    }
}

从上面简单的 demo 代码可以看出虽然是 get 请求,但是后端代码是会根据传的值做相应的动作,如果大家想知道这几个请求具体定义是什么可以参考其他资料学习,在这里就不进一步说明;

2.1、获取首页接口

获取首页接口文档如下,通过接口文档我们获取如下信息:

1、该接口是 get 请求,我们知道一般 get 请求都是获取资源数据;

2、通过响应数据结构观察出来它是一个 JSON 数据结构并且套嵌好几层 list 数据结构;

图片

3、进一步为了了解它为什么是这样的数据结构,可以看看它后端代码链路图就明白为什么这个是这样的数据结构,下图是获取首页后端连路图与部门代码,有人问为什么需要看链路图与代码,这是为了分析问题做知识积累,还有代码如果是性能瓶颈,连代码怎么调用关系都不懂,那么更无重下手怎么定位代码问题,并给出合理的调优建议:

(uml类图与代码图)

图片

在看上面代码调用关系之前,先了解 下java EE 项目一般开发模式是什么,下图是简单介绍下

图片

说明:

  • controller是可接收和返回数据给用户的web层;
  • service 是业务逻辑层,处理数据,校验数据
  • dao 全名(data access object)是持久化层,专注于对数据库的操作的一层;
  • DB 就是数据库;

有上面知识铺垫后,再分析首页接口请求就会清楚明白,以首页带着大家了解后端代码怎么调用怎么实现;

1、web层能看到请求的资源路径,如图:

图片

2、业务实现层如下图,从图可以看出,该接口通过请求获取首页广告、获取推荐品牌、秒杀信息、新品推荐、人气推荐、专题数据一共是 6 个数据结构,通过代码就明白前端在请求后,前端展示接口响应为什么会有好几层数据结构数据;

图片

3、在往下查看Dao层数据,这里选择【获取推荐品牌】举例,其他都是类似查看方法,以下是 dao 层方法,注意该方法名【 getRecommendBrandList 】与 xml 中的 id 名称需要一一对应否则 xml 中的语句就找不 dao 层的方法,也就不能查出数据;

图片

具体 dao 层结构如下:

/**
 * 获取推荐品牌
 */
List<PmsBrand> getRecommendBrandList(@Param("offset") Integer offset,@Param("limit") Integer limit);

4、上面getRecommendBrandList 方法名对应的xml文件中的sql语句中的 id 关键字,通过下面很容易明白该xml文件就是一些sql语句,注意 Dao 层中的【 @Param("offset" 】需要与xml中的 @Param("offset") 做一一对应,这样才能传值正确。

<select id="getRecommendBrandList" resultMap="com.dunshan.mall.mapper.PmsBrandMapper.BaseResultMap">
    SELECT b.*
    FROM
        sms_home_brand hb
        LEFT JOIN pms_brand b ON hb.brand_id = b.id
    WHERE
        hb.recommend_status = 1
        AND b.show_status = 1
    ORDER BY
        hb.sort DESC
    LIMIT #{offset}, #{limit}
</select>

通过上面后端代码分析,明白后端调用关系,未来遇到代码性能问题,就知道怎么分析代码问题。

通过代码一步一步分析,这时就能看懂上面代码与uml类图。

接下来开始在Jmeter中编写获取首页信息接口,新建线程组,新建HTTP Request 请求,输入变量,根据响应结果添加断言,具体如下图;

图片

添加登陆态头信息,因为把商品加入购物需要登陆后才可以把商品加入购物车,所以脚本都需要增加头文件信息,以后脚本就不说明,脚本实现如下:

图片

验证接口是否成功,成功结果如下:

图片

用户登陆接口与用信息查询接口在之前的注册流程中已经讲解,这就不再这里说明Jmeter中实现,需要注意的是为什么在下单流程中需要用户登陆与用户信息,这是因为后面接口需要用户登陆态token与用户地址id号信息,所以需要使用该接口信息,如果未来需要做基准测试场景,可以把该信息保存起来,下次接口需要该信息即可通过参数化实现。

2.2、购物车添加商品接口

先看下接口文档,再分析需要什么参数,再决定怎么编写相应的Jmeter脚本,下图是商品加入购物车接口,分析该接口需要一个商品skuCode码还有一个是数量,说明下,实际工作中很少用户自己添加skuCode码一般是商品加入购物车,系统自动把商品 skuCode码添加到购物车中,数据库中或者缓存中自动增加一条记录。为什么需要增加到数据库或者缓存中,这是方便用户下次进入购物车该商品数据还在,如果不添加数据库或者缓存关闭浏览器或者手机浏览器等信息商品数据自然会丢失。

如果商品信息添加到cookie里面,要知道cookie数据是存放浏览器或者其他缓存中,该数据是依赖缓存,如果缓存消逝数据自然消失,所以数据需要存放的服务端,但是服务端依赖用户登陆态,如果有登陆态信息,系统会自动保留数据,下次用户登陆自然就能看到该商品信息,如果没有登陆态下次打开浏览器数据自然就不存在。

数据存放到cookie中只是临时存储,当用户离开,商品信息就会丢失,所以当用户离开的时候提示是否需要保留商品信息。

注意,添加商品信息要想下次能看得到就必须让用户登陆才能保留该商品信息。

图片

分析商品加入购物车需要商品skuCode与数量,而且该商品需要有库存才能加入成功,如果商品存在,但是没库存也不行,一般这些数据都是业务提供或者自己熟悉业务通过sql查询出商品信息再通过参数化做脚本。

通过表结构分析,库存表是【 pms_sku_stock 】通过sql语句就可以查出需要参数化的商品skuCode码;获取想要的数据后只要把数据另存为文本或者csv等存储中,下次做脚本时把参数加进去就行,关于参数化请参考《性能测试实战30讲》中的Jmeter中如何设置参数化数据?在这里就不详细说明怎么参数化。

下图是商品库存信息表:

图片

2.3、购物车最终添加商品接口

参数文件参考如下,对于 CSV Data Set Config 组件怎么使用请参考《性能测试实战30讲》中的如何做参数化,在这里就不做过多讲解怎么使用。

图片

最终脚本如下:

图片

2.4、查询购物车信息接口

通过观察查询购物车接口是get请求,与之前做的是一样,就不再多介绍,但是观察我们下单流程中的下一步是确认购物车商品接口里面需要传一个数组参数,数组参数就是这次查询出来的结果购物车信息中的 ID 值,目前查询购物车接口是 json 数组,通过Jmeter中的 JSON Extractor 可以获取全部购物车信息的中id号,再通过后置处理器中的【 BeanShell PostProcessor 】处理后可以把结果传给下一步做参数。

接口文档如下:

图片

相应结果如下:

{
 "code": 200,
 "message": "操作成功",
 "data": [
   {
     "id": 63280,
     "productId": 27,
     "productSkuId": 98,
     "memberId": 90,
     "quantity": 1,
     "price": 249,
     "productPic": "https://perfo7d.oss-cn-beijing.aliyuncs.com/mall/images/20200923/web.png",
     "productName": "",
     "productSubTitle": "",
     "productSkuCode": "201808270027001",
     "memberNickname": "xingneng_test",
     "createDate": "2020-11-02T15:28:56.000+00:00",
     "modifyDate": "2020-11-02T15:28:56.000+00:00",
     "deleteStatus": 0,
     "productCategoryId": 7,
     "productBrand": "",
     "productSn": "No86577",
     "productAttr": "[{\"key\":\"颜色\",\"value\":\"黑色\"},{\"key\":\"容量\",\"value\":\"32G\"}]"
   }
 ]
}

查询购物接口Jmeter脚本参考如下:

图片

验证结果:

图片

获取全部商品id号【JSON Extractor 】 写法参考如下图,* 是正则表达式,-1表示获取全部数据。

图片

BeanShell PreProcessor参考如下图:

图片

// log.info("调试是否获取长度id:"+vars.get("cartId_matchNr"));
int num=Integer.valueOf("${cartId_matchNr}");
 //log.info("数据为:"+num);
StringBuilder stringBuilder = new StringBuilder();
for(i = 1;i<=num;i++){
	stringBuilder.append(vars.get("cartId_"+i)+",");
}
String ids =  stringBuilder.substring(0, stringBuilder.length() - 1);
 //log.info("结果:"+ids);
vars.put("ides",ids);

解释说明:代码中的${cartIds_matchNr}是通过添加 Debug Sampler 组件运行后在View Results Tree 点击Debug Sampler 请求就能看到该数据,目前通过BeanShell脚本处理后,购物车确认订单接口就可以使用该值做参数化。

BeanShell处理验证结果如下:

图片

2.5、购物车确定订单接口

流程图:

图片

接口文档:

图片

根据购物车信息生成确认单信息接口脚本编写参考如下:

图片

注意:${ides} 参数是通过购物车查询获取的id号并且通过BeanShell 脚本处理后得到的参数结化,如果不清除请仔细查看上面脚本,就明白这个地方的参数怎么写。

[${ides}]

验证结果: 图片

2.6、生成订单接口

图片

根据上面接口文档可以观察出该接口需要传购物车id号目前上一步已经实现,用户收货地址这个值在注册的时候已经添加,并且也可以通过用户信息接口查询该值,剩下的就是优惠劵、积分这个两个值,目前这次不牵涉这两个值,是否可以不传或者传什么值,根据下面代码可以观察处理,可以为null;

图片

这次脚本中把 useIntegration 与 couponId 这两个参数直接去掉,之后Jmeter脚本参考如下:

图片

脚本代码参考如下:

{
  "cartIds": [${ides}],
  "memberReceiveAddressId": ${userId},
  "payType": 0
}

响应结果如下:

图片

2.7、分页查询订单信息接口

为什么要把分页订单查询这接口做为这次压测接口,这是因为在实际购买商品后,有时会直接查询自己之前的订单信息或者浏览器自己用户下的全部订单信息,这时候就会调用分页查询订单信息接口,这符合真实场景。

接口文档:

图片

说明:status根据接口文档提示只能输入【订单状态:-1->全部;0->待付款;1->待发货;2->已发货;3->已完成;4->已关闭】这几种状态值,后pageSize与pageNum很好理解是传什么值;我们也可以通过代码观察需要传什么值,代码如下:

图片

根据接口信息提示即可在Jmeter中实现,脚本参考如下;

图片

验证结果:

图片

2.8、支付订单接口

接口文档如下:

图片

说明:

该订单号是根据生成订单信息接口响应结果生成的订单号,如果需要使用订单号,就需要通过关联才能把订单号取出来,并且传给该接口使用,如果是基准场景测试是需要先造一批订单号做参数,再跑订单接口,

具体接口实现参考如下:

生成订单信息接口中获取订单号写法参考如下,需要说明的是用该组件取值需要放到【生成订单信息】下,支付接口才能获取该订单号做参数。

图片

支付订单接口开发参考如下:

图片

脚本验证结果:

图片

2.9、根据ID获取订单详情接口

通过接口文档看到订单详情只要传订单号就可以查出订单详细信息;

图片

在 Jmeter 中实现如下:

图片

验证查询结果如下:

图片

三、总结

1、通过注册流程与下单流程脚本开发,相信大家能掌握 Jmeter 做脚本开发能在 Jmeter 实现 get 与 post 请求、参数化、关联、还能通过【 JSR223 Sampler 】组件把响应结果保存到本地,还能通过后置处理【BeanShell PostProcessor 】处理响应参数,把参数处理自己想要的结果,最后把参数传给下一个接口使用;

2、通过添加【 Debug Sampler 】组件可以学到怎么调试接口请求,还可以通过该组件观察参数化取值变化;

3、通过使用首页接口做例子,一步一步带大家察看后端代码是怎么调用与代码之间的逻辑关系。如果性能测试中发现方法慢可以在方法前后输入时间戳,最后输出到日志中,在日志中查看该方法执行多少时间;

有上面的基础知识相信接下来遇到需要开发常见的性能脚本是没问题的。

四、问题

  1. 什么情况下会需要用 JSR233 Sampler 来保存数值到文件?
  2. 什么情况下会需要用 Debug Sampler?