我接触过许多性能测试工程师,大多数这类人工作在系统顶层,俯瞰整个系统的轮廓,通过掌握各类测试性能相关的工具,寻找表象的系统瓶颈,一旦定位准确,便开始一层层的从表象向下挖掘,最终甚至跟踪到某一个函数或变量,这基本是他们所能到达的极限了, 通常情况下,作为性能测试人员对代码级缺陷所导致的整个系统的性能问题往往只能感到望而兴叹。
解决这类问题的方法就是性能测试可以尽早的介入整个软件开发项目,在代码层进行性能测试是势在必行的。然而现实是面临着诸多问题,其中最基本的两个问题是:
(1)怎么测的问题:有一些传统的方法,即在一段代码的开始和结尾处插入计时器,这类方法简单易行,但要想覆盖被测对象,就会带来诸如零散、繁琐、不易管理、很难统计、工作量大等问题;另外,想要加一些负载并发,还需要硬编码或借助一些测试工具完成。
(2)谁来测的问题:单元测试的职责往往十分模糊,谁来对其负责呢,这往往成为一个烫手的山芋,一些敏捷项目会引入TDD方式,来协助对需求进行明确,并保障代码沿着这些目标得到实现,但这也只是从侧面加固了代码质量,很难涉及到对性能方面的考量,单元功能测试工作量巨大,这往往成为研发和测试人员之间互相推诿的内在原因,更不要提加入性能测试。
为了试图解决上述所面临的棘手问题,本文将引入一种思路,来介绍如何利用单元测试框架及一些小技巧,灵活和快速的构建基于单元功能测试代码之上的性能测试,将已经编写好的基于JUnit3的TestSuite快速地转化为简单易行的负载测试,甚至压力测试。
首先,先来看我们即将实现的一些针对代码性能测试的需求描述:
(1)时限测试(TimedTest)描述
使用TimedTest,可以执行有相关时间限制的测试——如果超过了该限度,就认为测试是失败的(即便测试逻辑本身实际上是成功的)。在测试对于业务致关重要的方法时,时限测试相比其他测试来说,在确定和监控性能指数方面很有帮助。甚至可以测试得更加细致一些,可以测试一系列方法来确保它们满足特定的时间限制。
(2)负载测试(LoadTest)描述
使用 LoadTest,可以指定要模拟的用户(线程)数量,并设置预期时限,甚至为这些线程的启动提供各类计时控制机制(如负载增长的步长、爬坡时间、预热时间等)。通过为 LoadTest 提供计时控制机制,可以更真实地模拟用户负载。
然后,引入实现中的一些认知与小技巧:
(1)代码实现要利用Decorator设计模式,即继承TestDecorator类进行TimedTest和LoadTest业务代码的实现;
(2)利用声明(Annotation),结合Java反射机制,达到只需简单的对TestSuite进行声明便可以在原有单元功能测试代码之上构建性能测试(这点类似于JUnit4中所引入的声明机制),并利用JUnit框架自动实现对所声明的TestSuite进行有效管理;
(3) LoadTest中保证运行启动的测试线程,在任何时候保持异步,即保证并发性;
(4)若引入爬坡时间、预热时间的计时控制机制,建议采用Timer而不是Thread.sleep,因为Timer更加精准。
最后,展现根据以上思路所进行编码的细节,这里只是一些简单的实现,但已经基本实现了我们的需求,代码内容主要包含三个部分:
声明(Annotation)代码片段:
import java.lang.annotation.Target;
import java.lang.annotation.Retention;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Inherited;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface TimedTest {
long eTime();//预期运行或响应时间
}
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LoadTest {
int vUser();//启动并行线程数
int eTime();//预期运行或响应时间
}
性能测试业务代码片段:
import junit.framework.AssertionFailedError;
import junit.framework.Test;
import junit.framework.TestResult;
import junit.extensions.TestDecorator;
/**
* 继承TestDecorator类,将TestCase装饰成一个TimedTester
* @author xreztento@vip.sina.com
*
*/
public class TimedTester extends TestDecorator{
private long eTime = 0;
private String decoratedName = "";
/**
* 构造函数
* @param test 被装饰的TestCase类
* @param eTime 期待的时间
* @param decoratedName 被装饰的类名
*/
public TimedTester(Test test, long eTime, String decoratedName){
super(test);
this.eTime = eTime;
this.decoratedName = decoratedName;
}
@Override
public void run(TestResult result){
long startTime = System.currentTimeMillis();//记录启动运行时间
super.run(result);//运行测试集
long endTime = System.currentTimeMillis();//记录终止运行时间
long aTime = endTime - startTime;//计算实际运行时间
if(eTime < aTime){//比较预期时间与实际时间,如果实际时间超过预期时间
result.addFailure(this, new AssertionFailedError(decoratedName
+ " eTime: " + eTime + "ms" + " aTime: " + aTime + "ms"));
}
}
}
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import junit.extensions.TestDecorator;
import junit.framework.AssertionFailedError;
import junit.framework.Test;
import junit.framework.TestResult;
/**
* 继承TestDecorator类,实现Callable接口,将TestCase装饰成一个LoadTester,并将自身包装成一个Callable,实现Executors
* @author xreztento@vip.sina.com
*
*/
public class LoadTester extends TestDecorator implements Callable
{
private int vUser = 0;
private long eTime = 0;
private String decoratedName = "";
private TestResult result = null;
private int MAX_THREAD = 1000;
/**
* 构造函数
* @param test 被装饰的TestCase类
* @param vUser 并行线程数
* @param eTime 期待的时间
* @param decoratedName 被装饰的类名
*/
public LoadTester(Test test, int vUser, long eTime, String decoratedName){
super(test);
this.vUser = vUser;
this.eTime = eTime;
this.decoratedName = decoratedName;
}
/**
* 实现Callable接口的call方法,返回单线程执行的时间
*/
@Override
public Long call() {
long startTime = System.currentTimeMillis();//记录启动运行时间
super.run(result);//运行测试集
long endTime = System.currentTimeMillis();//记录终止运行时间
long aTime = endTime - startTime;//计算实际运行时间
return aTime;
}
@Override
public void run(TestResult result){
long totalTime = 0;//所有线程运行时间
long avgTime = 0;//平均时间
this.result = result;
ExecutorService exec = Executors.newFixedThreadPool(MAX_THREAD);
List
> callableList = new ArrayList
>(vUser); List
> futureList = new ArrayList
>(vUser); for(int i = 0; i < vUser; i++){ callableList.add(this); } try { futureList = exec.invokeAll(callableList);//启动全部并行线程执行测试 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); exec.shutdownNow(); } for(Future
f : futureList){ try { totalTime += f.get();//计算总时间 } catch (InterruptedException | ExecutionException e) { // TODO Auto-generated catch block e.printStackTrace(); } } exec.shutdown(); avgTime = totalTime / vUser; //计算平均时间 System.out.println(decoratedName + "avgTime is " + avgTime + "ms"); if(eTime < avgTime){//比较预期平均时间与实际平均时间,如果实际时间超过预期时间 result.addFailure(this, new AssertionFailedError(decoratedName + " eTime: " + eTime + "ms" + " avgTime: " + avgTime + "ms")); } } }
反射构筑代码片段:
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import junit.framework.Test;
import junit.framework.TestSuite;
import java.util.Set;
import java.util.HashSet;
public class TestConstructer extends TestSuite{
private static Set
> clazzSet = new HashSet
>(50);//被测试类集合(继承JUnit3 TestCase的子类 )
/**
* 初始化被测试类集合
* @param clazzSet 被测试类集合对象
*/
public static void initialize(Set
> clazzSet){
TestConstructer.clazzSet = clazzSet;
}
/**
* 利用Java反射机制,根据Annotation生成对应的JUnit Test Case并通过TestSuite返回
* @return 符合JUnit3 TestSuite
* @throws InstantiationException
* @throws IllegalAccessException
* @throws IllegalArgumentException
* @throws InvocationTargetException
* @throws NoSuchMethodException
* @throws SecurityException
*/
public static Test suite() throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException{
TestSuite suite = new TestSuite();
//遍历被测试类集合对象
for(Class
clazz : clazzSet){
if(clazz.isAnnotationPresent(TimedTest.class)){//如果类被声明为TimedTest测试类型
TimedTest tt = clazz.getAnnotation(TimedTest.class);
long eTime = tt.eTime();//取得期望时间值
Method m = clazz.getDeclaredMethod("suite");
Object obj = clazz.newInstance();
suite.addTest(new TimedTester((Test)m.invoke(obj),eTime,clazz.getName()));//将一个TimedTester加入TestSuite
} else if(clazz.isAnnotationPresent(LoadTest.class)) {//如果类被声明为TimedTest测试类型
LoadTest lt = clazz.getAnnotation(LoadTest.class);
int vUser = lt.vUser();//取得并行线程数
long eTime = lt.eTime();//取得期望的平均时间值
Method m = clazz.getDeclaredMethod("suite");
Object obj = clazz.newInstance();
suite.addTest(new LoadTester((Test)m.invoke(obj),vUser,eTime,clazz.getName()));//将一个LoadTester加入TestSuite
}
}
return suite;
}
}
将以上代码片段打包,引用到我们单元功能测试项目中,为想要执行性能测试的TestSuite类,我们只需添加对应的声明:
(1)一个被声明为LoadTest的TestSuite,模拟并发用户数10个,期望通过时间为20000ms
import org.xrez.tester.TimedTest;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
@TimedTest(eTime = 300)
public class TestCase2 extends TestCase{
public void testA(){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void testB(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static Test suite(){
TestSuite suite = new TestSuite();
suite.addTestSuite(TestCase2.class);
return suite;
}
}
(2)一个被声明为TimedTest的TestSuite,期望通过时间为300ms
import org.xrez.tester.TimedTest;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
@TimedTest(eTime = 300)
public class TestCase2 extends TestCase{
public void testA(){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void testB(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static Test suite(){
TestSuite suite = new TestSuite();
suite.addTestSuite(TestCase2.class);
return suite;
}
}
我们还需要一个启动器,来注册并运行我们所声明的性能测试用例:
import java.lang.reflect.InvocationTargetException;
import java.util.HashSet;
import java.util.Set;
import org.xrez.tester.TestConstructer;
import junit.framework.Test;
import junit.framework.TestSuite;
public class Tester {
public static Test suite(){
TestSuite suite = new TestSuite();
Set
> clazzSet = new HashSet
>(10);
try {
clazzSet.add(Class.forName("TestCase1"));
clazzSet.add(TestCase2.class);
//注:可以通过以上两种简单的方式对反射类进行初始化,当然在测试项目庞大的情况下,可以通过遍历方式初始化
TestConstructer.initialize(clazzSet);
try {
suite.addTest(TestConstructer.suite());
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return suite;
}
}
好了,就此我们已经按照思路所规划的路径完成了符合我们所制定的代码级性能测试需求的一个简易框架,下面就可以直接利用JUnit框架运行启动器,来看看测试效果:
我们故意设置了一个不能通过期望通过时间阈值的TestSuite,利用JUnit框架便可以有效的将性能问题暴露出来,并只需要通过不断的对原有单元功能测试用例进行声明和声明参数的调整,就可以快速开展代码级性能测试,从而大大减少重复地工作量和管理成本。
尾声:
本文介绍了快速构建基于代码级性能测试方法的一种思路,并没有深入进行展开,笔者只是借此呼吁应对代码级性能测试提高关注,而不是轻易的将性能缺陷不断地深埋,等待着系统级测试中去挖掘。另外,文中对框架的简单实现可以直接使用,也可以在此基础上扩充测试业务代码的内容和声明的范围粒度。