最近遇到一个尴尬的问题,由于公司机测试环境的机房迁移,导致办公区的网络跟测试环境网络之前延迟比较大,大到什么程度呢?大到不能正常使用测试环境。

由于网络组一直在排查,暂时没有答复,所以只能采取一个比较临时的办法。我自己在本机用的​​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热更新的方式替换方法类,这个比较复杂,而且适用范围更窄,不可取。

公众号FunTester,非著名测试开发,文章记录学习和感悟,欢迎关注,交流成长。