近期计划做一个自动化回归测试的工具,大体实现的功能如下:
- 从测试环境抓取相关的日志、输入输出报文;
- 根据日志或报文来构造这个测试场景需要的数据,可以采用从数据库中抓取数据或从日志中解析数据;
- 根据输入输出报文来配置调用外部服务的挡板,挡板会负责校验输入项,并返回相应的输出;
- 将上述信息装配后,生成对应的测试案例。这个测试案例相当于我们应用自身的组件组装测试,可以用于回归我们自身的代码功能。
可以看出上面的测试主要为了还原测试环境的测试场景,通过使数据库、输入输出报文和测试环境完全一致来达到回归的目的。当然具体实现上还是有很多细节可以考虑的。目前考虑到的一个重要的问题就是时间的可重复性,很多测试案例都是依赖于特定的时间的,节假日、营业时间、夏令时等等,而每个测试案例自身对时间的需求又各异。这个问题是必须要解决的,其中一个方案是修改现有的代码,改为使用可测试性的时间类,例如springside的org.springside.modules.utils.time.ClockUtil
类;另一个方案是本文要介绍的,使用java agent的方式对应用的字节码进行修改,使其中的时间具有可测试性,这种方案的好处是无需修改应用代码,做到应用透明。
具体java agent的详细使用可以参考 ,这种技术在很多优秀工具中都有使用,比如Jmockit、JaCoCo、JRebel等场景下都有使用。
具体实现代码包括如下几部分:
## 1.代理类的main类
//file-name:FtmAgent.java
package com.zyh.ftmagent;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class FtmAgent {
public static void premain(String agentArgs, Instrumentation inst)
throws ClassNotFoundException, UnmodifiableClassException{
inst.addTransformer(new TimeTransformer());
}
}
2.字节码转换类
这里的处理分为两部分,一个是找到需要加工的类的方法;另一个是对这些方法进行修改,将其中的调用一般方法和new
形式的字节码进行修改。字节码修改部分采用Javassist来编写,它相对于其他工具如ASM更加易用,相关教程可以参考Javassist Tutorial。
//file-name:TimeTransformer.java
package com.zyh.ftmagent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtBehavior;
import javassist.CtClass;
import javassist.NotFoundException;
import javassist.expr.ExprEditor;
import javassist.expr.MethodCall;
import javassist.expr.NewExpr;
public class TimeTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// 仅对于com.zyh.test包下的类进行替换
if (className.startsWith("com/zyh/test")) {
byte[] transformed = null;
System.out.println("Transforming " + className);
ClassPool pool = ClassPool.getDefault();
CtClass cl = null;
try {
cl = pool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer));
if (cl.isInterface() == false) {
CtBehavior[] methods = cl.getDeclaredBehaviors();
for (int i = 0; i < methods.length; i++) {
if (methods[i].isEmpty() == false) {
doMethod(methods[i]);
}
}
transformed = cl.toBytecode();
}
} catch (Exception e) {
System.err.println("Could not instrument " + className + ", exception : " + e.getMessage());
} finally {
if (cl != null) {
cl.detach();
}
}
return transformed;
}
return null;
}
private void doMethod(CtBehavior method) throws NotFoundException, CannotCompileException {
// 修改方法体的定义
method.instrument(new ExprEditor() {
// 修改一般的方法调用语句
public void edit(MethodCall m) throws CannotCompileException {
try {
if (m.getClassName().equals("java.lang.System")) {
if (m.getMethodName().equals("nanoTime")) {
m.replace("{long rslt = com.zyh.ftmagent.MySystemTime#nanoTime(); $_ = ($r)rslt; }");
} else if (m.getMethodName().equals("currentTimeMillis")) {
m.replace(
"{long rslt = com.zyh.ftmagent.MySystemTime#currentTimeMillis(); $_ = ($r)rslt; }");
}
}
} catch (Exception ex) {
ex.printStackTrace();
throw new RuntimeException(ex);
}
}
// 修改new Date()这种格式的代码,替换其中的class类
public void edit(NewExpr e) throws CannotCompileException {
try {
if (e.getClassName().equals("java.util.Date")) {
e.replace("{$_ = new com.zyh.ftmagent.MyDate();}");
}
} catch (Exception ex) {
ex.printStackTrace();
throw new RuntimeException(ex);
}
}
});
}
}
3.自定义的日期类
包括两个类,具体如下:
- MyDate:对默认
Date
类进行了扩展,实现根据MysystemTime
获取的当前时间进行构造Date类。
- MySystemTime: 实现了
currentTimeMillis
、nanoTime
方法,会根据最初设置的当前时间间隔进行设置。为了保证线程安全,这里使用了ThreadLocal类,可以在程序中的公共部分调用setTimeDiff
方法对时间进行重新定义。
//file-name: MyDate.java
package com.zyh.ftmagent;
import java.util.Date;
public class MyDate extends Date {
private static final long serialVersionUID = 1L;
public MyDate(){
super(MySystemTime.currentTimeMillis());
}
}
//file-name:MySystemTime.java
package com.zyh.ftmagent;
public class MySystemTime {
//默认为当前时间
private static final Long INIT_VALUE = 0l;
private static ThreadLocal<Long> timeDiff = new ThreadLocal<Long>() {
@Override
protected Long initialValue() {
return INIT_VALUE;
}
};
public static long currentTimeMillis() {
return System.currentTimeMillis() - timeDiff.get();
}
public static long nanoTime() {
return System.nanoTime() - timeDiff.get() * 1000;
}
/**
* 设置距离当前的时间,为正表示后退多少ms
*
* @param diff
*/
public static void setTimeDiff(long diff) {
timeDiff.set(diff);
}
}
4.MANIFEST.MF文件
其中的Premain-Class
为FtmAgent
的主类,在代码编译好后需要把上述的所有代码打成jar包(例如包名为ftm-agent.jar
),以备使用。
Manifest-Version: 1.0
Premain-Class: com.zyh.ftmagent.FtmAgent
Boot-Class-Path: javassist-3.22.0-CR1.jar
测试及其他
测试类需要在com.zyh.test
包下,测试前需要执行MySystemTime.setTimeDiff
来将时钟进行回拨。如在eclipse中测试需要在vm的参数中增加 -javaagent=ftm-agent.jar
,然后执行测试的main函数即可。完整的代码见FtmAgent-Gist。
参考资料: