经过了之前方案一和方案二的铺垫,方案三也呼之欲出,这就敬上。
首先我们照例先回顾一下之前两个方案的设想和实现文章:
- 分布式性能测试框架用例方案设想(一)
- 基于 docker 的分布式性能测试框架功能验证(一)
- 高 QPS 下的固定 QPS 模型
- 分布式性能测试框架用例方案设想(二)
- 基于 docker 的分布式性能测试框架功能验证(二)
基于脚本的压测场景
性能测试脚本基于FunTester
性能测试框架,在之前的方案二
中,我们需要将用例写进去基础的jar
包中,然后通过反射调用,灵活之处就是可以将用例的主要变量参数化,但是死板之处就是用例整体的设计已经完成了,参数化能力有限。如果用例场景需要增添,这种方式也就无能为力了,又时候甚至一点点的用例改动都会造成用例失效的情况。
这里我们需要一种更加灵活的用例形式和运行形式。那就是直接用服务运行测试脚本,这里的脚本分成Groovy
脚本和Java
脚本,得益于Groovy
强大的兼容能力,直接把Java
脚本当做Groovy
脚本大部分情况下都是 OK 的。
这里需要使用者在本地编辑好脚本之后,把脚本内容上传到服务器,由性能测试服务直接运行脚本而得的测试结果。这里我排除了文件上传,因为太麻烦了,管理文件会让事情变得更加复杂,再者脚本的文件长度并不长,可以存在数据库或者Git
上,这里说一下ngrinder
就是这么干的。
这类用例的不需要部署,直接可运行。方便存储,方便管理,也方便修改,目前来讲是兼容了所有方案一和方案二的场景,几乎适用于所有的HTTP协议
接口测试,包括支持更多的用例场景,包括单链路、多链路和全链路测试。对于每次请求都需要签名的接口也是非常不错的选择。这个基本就是FunTester测试框架对于HTTP
协议接口测试的终极解决方案。
PS:后续我会加强dubbo
、Socket
、Redis
、MySQL
等协议的支持,此外包括支持多脚本之间的参数传递功能。
实现 Demo
这里我自己写了一个测试类,实现了无参方法、基础类型参数方法、String 对象类型参数、String 数组类型参数四种方法的反射执行的Demo
,功能基于com.funtester.frame.execute.ExecuteSource
类,这个主要功能就是执行jar
包内的方法,这个类的代码我会放在最后,大家也可以点击阅读原文查看仓库中的最新代码。
下面是测试类的代码,其实就是一个简单的单接口测试脚本:
import com.funtester.config.Constant
import com.funtester.frame.execute.Concurrent
import com.funtester.frame.thread.RequestThreadTimes
import com.funtester.httpclient.ClientManage
import com.funtester.httpclient.FunLibrary
import com.funtester.utils.ArgsUtil
import org.apache.http.client.methods.HttpGet
class Share extends FunLibrary{
public static void main(String[] args) {
ClientManage.init(10, 5, 0, EMPTY, 0);
def util = new ArgsUtil(args)
int thread = util.getIntOrdefault(0,20);
int times = util.getIntOrdefault(1,100);
String url = "http://localhost:12345/m";
HttpGet get = getHttpGet(url);
Constant.RUNUP_TIME = 0;
RequestThreadTimes task = new RequestThreadTimes(get, times);
new Concurrent(task, thread, "本地固定QPS测试").start();
testOver();
}
}
下面是执行方法:
String s = RWUtil.readTxtByString("/Users/oker/IdeaProjects/funtester/src/test/groovy/com/funtest/groovytest/Share.groovy");
ExecuteGroovy.executeScript(s);
如果想对脚本进行参数化,例如我讲线程数和请求次数以及软启动时间都进行了参数化处理,那么得需要重新写一个方法,因为直接脚本包括反射是无法有效识别String[]
类型的参数的。
需要添加一个方法,如下:
public static void test(String params) {
main(params.split(COMMA))
}
最终通过请求这个方法达到参数化的目的,执行如下:
ExecuteGroovy.executeFileMethod("/Users/oker/IdeaProjects/funtester/src/test/groovy/com/funtest/groovytest/Share.groovy", "test", "20,100");
这里主要考虑到有可能会手动在服务器上执行测试用例,所以将用例内容写在了main
方法中,还有一种Groovy
脚本的语法,就是直接写内容,不依赖类和方法,也是可以执行的。
import com.funtester.config.Constant
import com.funtester.frame.execute.Concurrent
import com.funtester.frame.thread.RequestThreadTimes
import com.funtester.httpclient.ClientManage
import com.funtester.httpclient.FunLibrary
import com.funtester.utils.ArgsUtil
import org.apache.http.client.methods.HttpGet
ClientManage.init(10, 5, 0, "", 0);
def util = new ArgsUtil(args)
int thread = util.getIntOrdefault(0, 20);
int times = util.getIntOrdefault(1, 100);
String url = "http://localhost:12345/m";
HttpGet get = getHttpGet(url);
Constant.RUNUP_TIME = 0;
RequestThreadTimes task = new RequestThreadTimes(get, times);
new Concurrent(task, thread, "本地固定QPS测试").start();
这个方案有个缺点就是难以进行参数化,需要结合groovy.lang.Binding
才行,这必然又使得使用更复杂一些,所以我暂时放弃了。后面计划的功能跨脚本传递参数的时候,估计绕不这个知识点。
用例创建
这里由于采用了脚本编写用例,其实之前的方案二中用例都是可以复用的,只是不用将用例编译打包而已。下面分享一个单链路测试案例,有兴趣可以穿越一下单链路性能测试实践文章中的链路设计和实现思路。这里只分享一下脚本内容,如下:
import com.alibaba.fastjson.JSON
import com.alibaba.fastjson.JSONObject
import com.funtester.base.bean.AbstractBean
import com.funtester.base.constaint.ThreadLimitTimesCount
import com.funtester.frame.execute.Concurrent
import com.funtester.httpclient.ClientManage
import com.funtester.utils.ArgsUtil
import com.okayqa.composer.base.OkayBase
import com.okayqa.composer.function.Mirro
import com.okayqa.composer.function.OKClass
class Login_collect_uncollect extends OkayBase {
public static void main(String[] args) {
ClientManage.init(10, 5, 0, "", 0)
def util = new ArgsUtil(args)
def thread = util.getIntOrdefault(0, 30)
def times = util.getIntOrdefault(1, 40)
def tasks = []
thread.times {
tasks << new FunTester(it, times)
}
new Concurrent(tasks, "资源库1.4登录>查询>收藏>取消收藏链路压测").start()
allOver()
}
private static class FunTester extends ThreadLimitTimesCount<Integer> {
OkayBase base
def mirro
def clazz
FunTester(Integer integer, int times) {
super(integer, times, null)
}
@Override
void before() {
super.before()
base = getBase(t)
mirro = new Mirro(base)
clazz = new OKClass(base)
}
@Override
protected void doing() throws Exception {
def klist = mirro.getKList()
def karray = klist.getJSONArray("data")
K ks
karray.each {
JSONObject parse = JSON.parse(JSON.toJSONString(it))
if (ks == null) {
def level = parse.getIntValue("node_level")
def type = parse.getIntValue("ktype")
def id = parse.getIntValue("id")
ks = new K(id, type, level)
}
}
JSONObject response = clazz.recommend(ks.id, ks.type, ks.level)
def minis = []
int i = 0
response.getJSONArray("data").each {
if (i++ < 2) {
JSONObject parse = JSON.parse(JSON.toJSONString(it))
int value = parse.getIntValue("minicourse_id")
minis << value
}
}
clazz.unCollect(random(minis))
mirro.getMiniCourseListV3(ks.id, ks.type, 0, ks.level)
}
}
private static class K extends AbstractBean {
int id
int type
int level
K(int id, int type, int level) {
this.id = id
this.type = type
this.level = level
}
}
}
用例传输
上传用例
其实就是把用例当做字符串String
类型对象即可,上传用例、保存用例、编辑用例也都可以按照这个思路。
分配用例
分配用例其实就是将用例的分配到slave
节点去运行,这里我采用了ngrinder
的方式,不在去处理用例的自定义参数,包括不限于线程数、请求次数、请求时间、软启动时间等等,而是通过指定运行节点个数来控制压力倍数。
用例执行
如通本文开始实现Demo
中所写,就是执行测试任务中具体用例的方法了。执行类的代码如下:
package com.funtester.frame.execute;
import com.alibaba.fastjson.JSON;
import com.funtester.base.exception.FailException;
import com.funtester.frame.SourceCode;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
public class ExecuteSource extends SourceCode {
private static Logger logger = LogManager.getLogger(ExecuteSource.class);
/**
* 执行方法
* <p>防止编译报错,用list绕一圈</p>
*
* @param params
*/
public static Object executeMethod(List<String> params) {
Object[] objects = params.subList(1, params.size()).toArray();
return executeMethod(params.get(0), objects);
}
/**
* 执行方法
* <p>防止编译报错,用list绕一圈</p>
*
* @param params
*/
public static Object executeMethod(String[] params) {
return executeMethod(Arrays.asList(params));
}
/**
* 执行具体的某一个方法,提供内部方法调用
* <p>重载方法如果参数是基础数据类型会报错</p>
*
* @param path
* @param paramsTpey
*/
public static Object executeMethod(String path, Object... paramsTpey) {
int length = paramsTpey.length;
if (length % 2 == 1) FailException.fail("参数个数错误,应该是偶数");
String className = path.substring(0, path.lastIndexOf("."));
String methodname = path.substring(className.length() + 1);
Class<?> c = null;
Object object = null;
try {
c = Class.forName(className);
object = c.newInstance();
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
logger.warn("创建实例对象时错误:{}", className, e);
}
Method[] methods = c.getDeclaredMethods();
for (Method method : methods) {
if (!method.getName().equalsIgnoreCase(methodname)) continue;
try {
Class[] classs = new Class[length / 2];
for (int i = 0; i < paramsTpey.length; i +=2) {
classs[i / 2] = Class.forName(paramsTpey[i].toString());//此处基础数据类型的参数会导致报错,但不影响下面的调用
}
method = c.getMethod(method.getName(), classs);
} catch (NoSuchMethodException | ClassNotFoundException e) {
logger.warn("方法属性处理错误!");
}
try {
Object[] ps = new Object[length / 2];
for (int i = 1; i < paramsTpey.length; i +=2) {
String name = paramsTpey[i - 1].toString();
String param = paramsTpey[i].toString();
Object p = param;
if (name.contains("Integer")) {
p = Integer.parseInt(param);
} else if (name.contains("JSON")) {
p = JSON.parseObject(param);
}
ps[i / 2] = p;
}
method.invoke(object, ps);
} catch (IllegalAccessException | InvocationTargetException e) {
logger.warn("反射执行方法失败:{}", path, e);
}
break;
}
return null;
}
}
有一些暂时无用的方法我已删除,有兴趣的可以去仓库看看,点击阅读原文即可直达FunTester的仓库。