最近遇到一个尴尬的问题,由于公司机测试环境的机房迁移,导致办公区的网络跟测试环境网络之前延迟比较大,大到什么程度呢?大到不能正常使用测试环境。
由于网络组一直在排查,暂时没有答复,所以只能采取一个比较临时的办法。我自己在本机用的Java
写的测试框架以及Groovy
写的测试脚本,具体情况可参考:如何统一接口测试的功能、自动化和性能测试用例。
由于本人之前拥有的一台独立物理测试机被收回,现在分给测试组的只有一个docker
容器起来的服务。本来最优的方案是在docker file
文件时候吧Groovy SDK
加上去,保证一个Groovy
运行环境,但也被否掉了,只留了一个口子给我,就是上传文件到项目Git
中,然后通过够部署项目把文件弄到docker
容器中。
Groovy SDK
又比较大,完事儿还需要重新设置环境变量等等问题,我想到了两个其他方案:
- 将项目
build
成jar
包,测试用例(也就是某个类的main
方法),通过执行jar
包中的class
类的main
方法,达到执行不同测试用例的目的,顺手做一个参数化。 - 定义一个统一的
main
方法入口,通过反射执行不同的方法。
显然第二个思路用途更广,但是实现起来略微麻烦了一些,而且传参的时候比较复杂,个人建议还是优先考虑第一种方式。
下面分享这两种方式的实现。
执行class的main方法
首先我写一个测试用例,内容如下:
package com.okayqa.composer.performance.teach1_1
import com.fun.frame.execute.Concurrent
import com.fun.frame.httpclient.ClientManage
import com.fun.frame.httpclient.FanLibrary
import com.fun.frame.thread.HeaderMark
import com.fun.frame.thread.RequestThreadTimes
import com.fun.utils.ArgsUtil
import com.okayqa.composer.base.OkayBase
import com.okayqa.composer.function.IMSocket
class ActivityUnread extends OkayBase{
public static void main(String[] args) {
ClientManage.init(5, 5, 0, "", 0)
def util = new ArgsUtil(args)
def thread = util.getIntOrdefault(0, 100)
def times = util.getIntOrdefault(1, 100)
def base = getBase()
def socket = new IMSocket(base)
socket.getActivityUnread(81951375949,43519,43504)
def request = FanLibrary.getLastRequest()
def mark = new HeaderMark("requestid")
def times1 = new RequestThreadTimes(request, times, mark)
new Concurrent(times1, thread, "activity未读消息").start()
allOver()
}
}
然后使用Maven
的package
命令打包。执行Java
命令即可执行jar
包中某个class
的main
方法,可参数化。
java -cp okay_test-1.0-SNAPSHOT.jar com.okayqa.composer.performance.teach1_1.ActivityUnread 1 1 start
下面是输出:
INFO-> 当前用户:fv,IP:10.60.192.21,工作目录:/Users/fv/Documents/workspace/okay_test/target/,系统编码格式:UTF-8,系统Mac OS X版本:10.15.7
INFO-> requestid: Fdev1607495809625
INFO-> 请求uri:https://teacherpad-stress.xk12.cn/api/t_pad/user/login,耗时:368 ms
INFO-> 教师:61951375269,学科:null,名称:61951375269,登录成功!
INFO-> requestid: Fdev1607495810141
INFO-> 请求uri:https://ailearn-composer-interface-stress.xk12.cn/api/composer/activity/course_list/unread_num,耗时:106 ms
INFO->
~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~ JSON ~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~
> {
> ① . "data":{
> ② . . . "activity_unread_num":[
> ③ . . . . . {
> ③ . . . . . "activity_id":43519,
> ③ . . . . . "msg_count":208
> ② . . . },
> ② . . . {
> ③ . . . . . "activity_id":43504,
> ③ . . . . . "msg_count":0
> ③ . . . . . }
> ② . . . ]
> ① . },
> ① . "meta":{
> ② . . . "emsg":"成功",
> ② . . . "ecode":0
> ① . }
> }
~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~ JSON ~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~
INFO-> gc回收线程开始了!
INFO-> 线程:activity未读消息0,执行次数:1,错误次数: 0,总耗时:1 s
INFO-> 总计1个线程,共用时:0.059 s,执行总数:1,错误数:0,失败数:0
INFO-> 数据保存成功!文件名:/Users/fv/Documents/workspace/okay_test/target/long/data/1activity未读消息20201209143650
INFO->
~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~ JSON ~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~
> {
> ① . "rt":56,
> ① . "total":1,
> ① . "qps":17.857142857142858,
> ① . "failRate":0.0,
> ① . "threads":1,
> ① . "startTime":"2020-12-09 14:36:50",
> ① . "endTime":"2020-12-09 14:36:50",
> ① . "errorRate":0.0,
> ① . "executeTotal":1,
> ① . "mark":"activity未读消息20201209143650",
> ① . "table":""
> }
~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~ JSON ~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~
INFO->
INFO-> gc回收线程结束了!
完美执行1 !!!
反射执行方法
首先封装一个反射执行的工具类,代码如下:
package com.fun.frame.execute;
import com.alibaba.fastjson.JSON;
import com.fun.base.exception.FailException;
import com.fun.config.Constant;
import com.fun.frame.SourceCode;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@SuppressFBWarnings({"NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", "NP_NULL_ON_SOME_PATH_EXCEPTION"})
public class ExecuteSource extends SourceCode {
private static Logger logger = LoggerFactory.getLogger(ExecuteSource.class);
/**
* 执行包内所有类的非 main 方法
*
* @param packageName
*/
public static void executeAllMethodInPackage(String packageName) {
List<String> classNames = getClassName(packageName);
if (classNames != null) {
for (String className : classNames) {
String path = packageName + "." + className;
executeAllMethod(path);// 执行所有方法
}
}
}
/**
* 执行一个类的方法内所有的方法,非 main,执行带参方法的代码过滤
*
* @param path 类名
*/
public static void executeAllMethod(String path) {
Class<?> c = null;
Object object = null;
try {
c = Class.forName(path);
object = c.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
Method[] methods = c.getDeclaredMethods();
for (Method method : methods) {
try {
method.invoke(object);
} catch (IllegalAccessException e) {
logger.warn("非法访问导致反射方法执行失败!", e);
} catch (InvocationTargetException e) {
logger.warn("反射调用目标异常导致方法执行失败!", e);
} catch (Exception e) {
logger.warn("反射方法执行失败!", e);
} finally {
sleep(Constant.EXECUTE_GAP_TIME);
}
}
}
/**
* 提供给命令行main方法使用
*
* @param params
*/
public static void executeMethod(String... params) {
String[] ps = Arrays.copyOfRange(params, 1, params.length);
executeMethod(params[0], ps);
}
/**
* 执行具体的某一个方法,提供内部方法调用
*
* @param path
*/
public static void 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("方法属性处理错误!", e);
}
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 = new Integer(changeStringToInt(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;
}
}
/**
* 获取当前类的所有用例方法名
*
* @param path
* @return
*/
public static List<String> getAllMethodName(String path) {
List<String> methods = new ArrayList<>();
Class<?> c = null;
Object object = null;
try {
c = Class.forName(path);
object = c.newInstance();
} catch (Exception e) {
FailException.fail("初始化对象失败:" + path);
}
Method[] all = c.getDeclaredMethods();
for (int i = 0; i < all.length; i++) {
String str = all[i].getName();
methods.add(str);
}
return methods;
}
/**
* 获取某包下所有类
*
* @param packageName 包名
* @return 类的完整名称
*/
public static List<String> getClassName(String packageName) {
List<String> fileNames = new ArrayList<>();
ClassLoader loader = Thread.currentThread().getContextClassLoader();// 获取当前位置
String packagePath = packageName.replace(".", Constant.OR);// 转化路径,Linux 系统
URL url = loader.getResource(packagePath);// 具体路径
if (url == null || !"file".equals(url.getProtocol())) {
FailException.fail("获取包路径失败!");
}
File file = new File(url.getPath());
File[] childFiles = file.listFiles();
for (File childFile : childFiles) {
String path = childFile.getPath();
if (path.endsWith(".class")) {
path = path.substring(path.lastIndexOf(OR) + 1, path.lastIndexOf("."));
fileNames.add(path);
}
}
return fileNames;
}
}
使用Demo
如下:
package com.fun.main;
import com.fun.frame.SourceCode;
import com.fun.frame.execute.ExecuteSource;
public class ExecuteMethod extends SourceCode {
public static void main(String[] args) {
args = new String[]{"com.fun.ztest.java.T.test", "java.lang.Integer", "1"};
ExecuteSource.executeMethod(args);
}
}
其中T
的代码中test()
方法如下:
public static void test(int i) {
output(33333333 + i);
}
这里我模拟了args
参数,可以看出这里的参数非常复杂,都是较长的String
字符串。
控制台输出:
INFO-> 当前用户:fv,IP:10.60.192.21,工作目录:/Users/fv/Documents/workspace/fun/,系统编码格式:UTF-8,系统Mac OS X版本:10.15.7
WARN-> 方法属性处理错误!
java.lang.NoSuchMethodException: com.fun.ztest.java.T.test(java.lang.Integer)
at java.lang.Class.getMethod(Class.java:1786) ~[?:1.8.0_51]
at com.fun.frame.execute.ExecuteSource.executeMethod(ExecuteSource.java:106) [classes/:?]
at com.fun.frame.execute.ExecuteSource.executeMethod(ExecuteSource.java:77) [classes/:?]
at com.fun.main.ExecuteMethod.main(ExecuteMethod.java:10) [classes/:?]
INFO-> 33333334
Process finished with exit code 0
- 这里的报错是因为
test()
方法的参数是int
并不是我传入的java.lang.Integer
导致的,单并不影响后面的方法调用正常执行,可忽略。
完美执行2 !!!
- 还有一种神器可以解决这个问题:arthas,可以通过arthas命令redefine实现Java热更新的方式替换方法类,这个比较复杂,而且适用范围更窄,不可取。