服务端性能测试实战3-性能测试脚本开发
前言
在前面的两篇文章中,我们分别介绍了性能测试的理论知识以及性能测试计划制定,本篇文章将重点介绍性能测试脚本开发。脚本开发将分为两个阶段:
- 阶段一:了解各个接口的入参、出参,使用Python代码模拟前端发送http请求,重点是通过代码调用串联起各个接口,了解接口层面的基本流程,也为后续模拟压测打好基础;
- 阶段二:将代码的接口实现转移到性能测试工具中来实现,跑通流程,重点是做好细节实现以及接口关联设计
一、Python脚本开发
1.加解密函数编写
加密:GET、DELETE请求参数不需要加密,POST请求需要对参数进行加密;
解密:每个请求返回的都是密文,所以在接口调用时要对密文进行解密。
由于加解密逻辑较为复杂,且不具有普适性,因此,此处只作简单说明,不介绍具体逻辑实现。看似简单的两个加密、解密函数,其实还是花了不少时间和精力才搞出来。
def encrypt_aes(clear_text, aes_key):
"""
使用 AES 加密明文
:param clear_text: 明文
:param aes_key: AES 密钥
:return: 加密后的 Base64 编码字符串
"""
pass
def decrypt_aes(encrypt_text, aes_key):
"""
使用 AES 解密密文
:param encrypt_text: 密文(Base64 编码字符串)
:param aes_key: AES 密钥
:return: 解密后的字符串
"""
pass
2.封装接口
以快速创建接口为例:
def quick_start_meeting(self, auth_token, c_token, aes_key, tenant_id):
"""
快速创建会议
:param auth_token: 在头里用
:param c_token: 在头里用
:param topic: 会议主题
:param tenantId: 租户ID
:param scheduleTime: 日期时间, int
:param timezone: 时区
:return: 会议信息
"""
path = "/meeting/start"
headers = {"authtoken": auth_token, "Content-Type": "application/json", "ctoken": c_token, "ec": self.ec,
"ev": self.ev, "user-agent": self.user_agent, "deviceid": self.device_id}
data = {"topic": "GG Meeting" + str(random.randint(1000, 9999)), "scheduleTime": self.timestamp,
"timezone": "Asia/Shanghai", "tenantId": tenant_id}
enc_data = encrypt_aes(json.dumps(data), aes_key)
response = requests.post(self.url + path, headers=headers, json={"data": enc_data})
dec_data = decrypt_aes(json.dumps(eval(response.text)["data"]), aes_key)
print("会议创建成功")
return dec_data
代码解释:
- headers:组装请求头
- data:组装创建会议所需的参数;
- 调用加密方法encrypt_aes,传入会议参数和Key进行加密操作;
- 发起POST请求
- 调用解密方法decrypt_aes,传入处理过的返回值和Key进行解密操作;
- 返回解密后的响应体;
3.调用接口
把所有的接口封装完,我们来调用接口验证是否能调用通。
可以看到接口调用成功,返回了会议id、会议Number等信息。
4.参数构造及数据关联设计
接口参数通常会涉及到三种类型:
- 灵活传递参数的字段:这类参数需要做关联设计,这样可以保证数据是“活”的。在测试过程中,每个环境的数据都不一样,我们不能用写死的数据来发起请求,例如某个ID字段,在A环境有,但是B环境没有。每次都手动改数据的话,也不现实。从上一个接口取值传递给下一个接口,是接口测试过程中常用的数据关联做法,这样可以保证数据是实时且有效的。
- 唯一性校验的字段:例如商品编号、身份证号码等。对于这类字段,我们尽量使用自生成的数据,保证每次提交的数据都是唯一的,不会造成参数重复之类的冲突,同时也方便日志定位。尽量不要从CSV这类的文件中读取,因为只能读取一轮,第二轮再读取又会提交相同的数据了,同时也会增加操作的复杂度。
- 没有唯一性校验的字段:例如商品价格、人员年龄。这类字段没有唯一性校验,可以固定写死。
5.验证接口流程
先在代码中确保每个接口都能正常调用,再通过参数传递的方式将各个接口串联起来。接口流程畅通后,接下来就可以把各个接口腾到Jmeter中来实现。
二、Jmeter脚本开发
不同于在Python代码中实现,在Jmeter工具中,绝不仅仅是添加一些HTTP请求、把各个接口腾过来这么简单;首要问题就是解决参数的加解密问题,这一步搞不定其他都是白瞎。
我最初的想法是在Jmeter工具中使用前后置处理器调用Python加解密函数来对参数进行加密、返回值解密,但是折腾了大半天发现Jmeter对于Python代码的调用并不能很好的支持(虽然也可以安装Jython插件、在JSR223后置处理器中编写Python代码,但Jython插件只支持Python2.7的语法,后续可能会面临一系列麻烦),所以最后放弃了这个方案。
Jmeter是Java写的,所以Jmeter的前后置处理器天然对于Java语言支持最好。所以可以在JSR223后置处理器中调用Java加解密函数,来实现参数、返回值的加解密。
1.解决参数加解密问题
想要在JSR223后置处理器中调用Java加解密函数,来实现参数、返回值的加解密。必然要导入这个加解密的方法。具体实现思路是(这种方式适用于引用所有自定义的Java方法):
- 用Java语言编写加解密工具类
- 编译Java工具类代码,生成class文件
- 将class文件打为jar包
- 将jar包放到Jmeter指定扩展目录
- 在前后置处理器中导入加解密工具类
- 前置处理器中调用工具类中的加密方法对组装好的参数进行加密
- 后置处理器中调用工具类中的解密方法对返回密文进行解密
① 编写 AESUtil.java加解密工具类
package com.csb.meeting.crypto.utils;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
public class AESUtil {
// 加密算法RSA
public static final String KEY_ALGORITHM = "AES";
// 编码方式
public static final String CODE_TYPE = "UTF-8";
// 填充类型 AES/ECB/PKCS5Padding AES/ECB/ISO10126Padding
public static final String AES_TYPE = "AES/ECB/PKCS5Padding";
/**
* 加密
*
* @param clearText 明文
* @param aesKey AES秘钥
* @return 加密串
*/
public static String encryptAes(String clearText, String aesKey) {
...
}
/**
* 解密
*
* @param encryptText 密文
* @param aesKey AES秘钥
* @return 解密串
*/
public static String decryptAes(String encryptText, String aesKey) {
...
}
}
② 编译 AESUtil.java代码,生成class文件
确保 AESUtil.java 代码能成功编译。在命令行先使用 javac 进行编译,生成.class目录及文件:
javac -d . AESUtil.java
此命令会在当前目录下生成
com/meeting/crypto/utils/AESUtil.class 文件。
③ 打包class文件为jar包
再使用jar命令将.class目录及文件打成包jar包:
jar cf AESUtil.jar com
④ 将编译后的jar包添加到 JMeter 指定扩展路径
把生成的AESUtil.jar复制到 JMeter 的 lib/ext 目录下。这样 JMeter 就能在运行时找到 AESUtil 类了。
⑤ 在 JSR223 前/后置处理器中使用 Java 调用加解密方法
在 JMeter 中添加 JSR223 后置处理器,将语言选择为 Java,注意在开头一定要先导入加解密工具类“import
com.csb.meeting.crypto.utils.AESUtil;”,例如解密会议列表:
import com.meeting.crypto.utils.AESUtil;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.threads.JMeterVariables;
try {
// 获取 JMeter 变量
JMeterVariables vars = JMeterContextService.getContext().getVariables();
// 从 JMeter 变量中获取加密文本和 AES 密钥
String meetingList = vars.get("meetingList");
String aesKey = vars.get("aesKey");
// 检查变量是否为空
if (encryptedText == null || aesKey == null) {
System.err.println("加密文本或 AES 密钥为空,请检查用户定义的变量。");
return;
}
// 调用 decryptAes 方法进行解密
String decryptedText = AESUtil.decryptAes(meetingList, aesKey);
// 将解密后的结果保存到 JMeter 变量中
vars.put("decryptedText", decryptedText);
// 打印日志,方便调试
System.out.println("AES 密钥: " + aesKey);
System.out.println("解密后的数据: " + decryptedText);
}
catch (Exception e) {
log.error("Decryption failed: " + e.getMessage(), e);
}
查看控制台,成功解密数据并打印:
2.Jmeter脚本设计
① 添加用户自定义的变量
将一些不经常变化的公共参数抽离到用户自定义的变量中保存,后续调用时直接使用${variableName}进行调用即可。
② 添加HTTP公共请求头
将一些每个接口中都会带的请求头抽离到公共请求头中保存,这样每个接口在发起请求的时候都会带上这些请求头内容。
如果个别接口需要传递一些单独的请求头内容,再在该接口下添加一个单独的请求头即可。例如创建会议接口需要带上“ec”和“ev”:
③ 添加HTTP请求
添加HTTP请求,填写接口路径,参数等等。
④ 添加监听器
- 添加查看结果树:可以查看到每个请求的运行结果、请求参数、响应参数等信息,便于定位问题;
- 添加聚合报告监听器,用于查看每个接口的性能指标,如平均响应时间、吞吐量、错误率等;
- 添加图形结果监听器,以图形化方式展示接口的响应时间和吞吐量变化。
⑤ 添加前置处理器,加密参数
例如:创建会议接口需要先组装参数,再调用加密方法对参数进行加密。此时就需要在HTTP请求下添加“JSR223前置处理器”,语言选择Java,然后编写相应代码。
代码解释:
- 导入相应的包,尤其是自定义的加解密工具类;
- 从Jmeter全局变量中读取密钥和其他相应参数,用于加密操作;
- 引用系统自带的函数,生成自定义参数,用于接口参数组装;
- 调用工具类中的加密方法,对组装好的参数进行加密;
- 将加密后的密文保存到Jmeter全局变量中,HTTP请求中引用组装后的密文;
⑥ 添加断言
断言有多种类型,我只断言了接口返回值中的code字段是否等于200,基本上返回200就表示接口响应成功了(并不是HTTP响应状态码)。
⑦ 添加JSON提取器,提取返回值
由于接口返回的内容也是加密的,所以需要先把密文部分全部提取出来,后续再用后置处理器进行解密,提取指定字段的返回值。
⑧ 添加后置处理器,解密密文并保存
与前置处理器类似,需要在HTTP请求下添加“JSR223后置处理器”,语言选择Java,然后编写相应代码。
代码解释:
- 导入相应的包,尤其是自定义的加解密工具类;
- 从Jmeter全局变量中读取密钥和其他相应参数,例如前面保存的meetingInfo;
- 调用工具类中的解密方法,对meetingInfo进行解密;
- 再从解密后的内容中提取指定字段返回值,例如meetingNumber,
- 将meetingNumber保存到Jmeter全局变量中,后续的接口再添加前置处理器,从Jmeter全局变量中读取这个meetingNumber;
⑨ 添加循环控制器,模拟多个状态上报
其中会控接口和状态上报接口的参数,是个枚举值数组,不同的值代表不同动作,所以这里添加一个循环控制器,将会控接口和状态上报接口放进去,循环三次,每次都是取随机值,这样就可以模拟三种不同的状态上报和会控操作。
⑩ 添加随机定时器
添加随机定时器,模拟接口之间的延时,时间设置为100ms~150ms,类似于人手动点击某个页面(发起A请求)后,又点了某个按钮(发起B请求),二者之间的思考间隔。
3.验证Jmeter脚本
上述所有工作完成后,就可以运行脚本进行调试验证了。其实无论是写代码还是写Jmeter脚本,无论是小白还是大神,尤其是实现比较复杂的情况下,基本没有一次就运行通过的。在经历了无数次的“报错->排查定位->修改->再运行”后,看到全绿,就表示大功告成了!
至此,终于完成了脚本开发的阶段性目标。