UML软件工程组织

JUnitPerf

摘要

JUnitPerf是一个来度量代码的性能和执行效率的一个性能测试工具,通过编写用于JUnitPerf的单元测试代码可以使这一过程自动化。从另外一个角度来说它是JUnit的一个扩展插件。

假如你对这种类型的自动化测试感兴趣的话可以参考我写的书《Pragmatic Project Automation》

您也可以花两天时间光顾一下站点《Test-Driven Development with JUnit Workshop》,这里提供了一些很好的JUnit的学习途径并且还有Mike Clark的一些讲义和实践指导手册帮助你更深入地了解测试驱动开发。

简介

JUnitPerf是基于JUnit的一个度量性能和执行效率的一个自动化测试框架(工具)。

JUnitPerf包含以下两个主要的类(扩展了JUnit):

  • TimedTest
    TimedTest用来执行测试,返回执行该测试所使用的时间。
    TimedTest构造方法中需要指定一个最大可接受的执行时间。默认情况下,执行该方法时会等待被执行的测试执行完毕,如果实际所用的时间超过了指定的最大时间则标识测试失败。另外你也可以通过在构造方法指定当实际执行时间超过最大可接受时间时不继续执行该测试,并标识测试未通过。
  • LoadTest
    LoadTest用来模仿多个并发用户多次迭代执行测试。

使用目的

很明显,JUnitPerf是对JUnit测试框架的一个扩展。这种方式的扩展允许动态地增加JUnit测试用例来进行性能测试,不会影响到先前的测试。这样您就可以快速简易地构造出性能测试套件。

性能测试套件可以自动地,独立于其它的JUnit测试用例执行。实际使用中,一般要尽量避免把JUnit测试用例和JUnitPerf测试用例组织在一起,这样才能更加独立地执行测试套件,并且也可按不同的顺序执行。持续时间较长的性能测试可能会延长测试的时间,从而导致你不愿意去执行所有的单元测试。因此,这需要你有计划地不时地去执行该测试,而不必影响到其他工作。

JUnitPerf倾向于针对已经有明确的性能要求或者执行效率要求,并且要保证代码重构后依然保持这样的目标的测试。例如,您可以使用JUnitPerf测试来确保在同样的条件下不会由于改变算法而导致性能降低。您也可以使用它来确保重构一个资源池后不会导致在负载情况下的执行效率降低(这种保证是通过比较条件改变前后的执行时间和效率,只提供一个度量的依据)。

从投入产出的角度来看维护一个注重实效的测试是相当重要的。传统的性能度量工具和技术首先会去找出性能问题的潜在出处,而JUnitPerf则用来不断地自动测试并且检查需求和实际的结果。

以下是一个实际使用场景的例子:

你有一个功能良好的程序,并且通过了必要的JUnit测试套件的测试验证功能通过。从这个角度来说你已经达到了设计所想象的目标。

然后使用一个性能度量工具来分析程序的哪部分执行时间最长。基于设计知识,您已经具有很好的工具对程序做实际的评估。并且重构后的代码清晰简洁,接下来的工作就是调整一小部分代码。

接下来就可以写JUnitPerf测试用例了,为这部分代码指定可接受的性能和效率参数。如果不对代码做任何改动的情况下直接进行测试将不会通过,证明测试用例是正确的。接着对代码做一些小的调整。

每次调整后都重新编译和运行JUnitPerf测试。如果实际的性能到达了预期的指标,测试就算是通过了。如果实际的性能达不到预期的指标,就需要继续调整过程直到测试通过。如果将来代码再次重构了你也可以重新运行测试。如果测试未通过,而同时之前的性能标准也提高了,这时就需要回溯到原来并且继续重构直到测试通过。

JUnitPerf下载

JUnitPerf 1.9是当前最新的版本。包含以前所有版本的功能。

本版需要Java 2和JUnit 3.5或以上版本。

发行包包含一个JAR文件,源代码,示例代码,API文档和本文档。

JUnitPerf 安装

  • Windows
    在Windows上按以下步骤安装:
    1. 解压junitperf-<version>.zip文件到一个目录中,在系统环境变量中增加%JUNITPERF_HOME%,值为文件解压后的目录。
    2. 把JUnitPerf加到CLASSPATH路径中:
      set CLASSPATH=%CLASSPATH%;%JUNITPERF_HOME%\lib\junitperf-<version>.jar
  • Unix (bash)
    在UNIX上按以下步骤安装:
    1. 解压缩junitperf-<version>.zip到相应的目录下。例如:$JUNITPERF_HOME。
    2. 修改文件的权限:
      chmod -R a+x $JUNITPERF_HOME
    3. 把JUnitPerf加到CLASSPATH路径中:
      export CLASSPATH=$CLASSPATH:$JUNITPERF_HOME/lib/junitperf-<version>.jar

构建与测试

在$JUNITPERF_HOME/lib/junitperf-<version>.jar文件中已经包含有编译好的类文件。

  • 构建

    $JUNITPERF_HOME/build.xml文件是Ant构建文件。

    可以使用以下命令构建JUnitPerf:

    cd $JUNITPERF_HOME

    ant jar

  • 测试

    JUnitPerf安装包中包含了用于跟JUnitPerf结合使用的JUnit测试用例的实例。

    可以输入以下命令验证JUnitPerf安装是否正常:

    cd $JUNITPERF_HOME

    ant test

如何使用JUnitPerf

最好的方式是使用JUnitPerf中附带的示例,这里包含了各种类型的测试。

$JUNITPERF_HOME/samples目录包含了本文中所讲的所有示例代码.

  • TimedTest

    TimedTest构造方法有两个参数,一个是已存在的JUnit测试用例,另一个是预期的最大的执行时间。

    例如要针对ExampleTestCase.testOneSecondResponse()方法创建一个执行时间的测试并且等待该方法执行完毕,如果时间超过1秒则视为未通过。

    long maxElapsedTime = 1000;

    Test testCase = new ExampleTestCase("testOneSecondResponse");

    Test timedTest = new TimedTest(testCase, maxElapsedTime);

    同样地,如果想要在执行过程如果超出预期时间立即结束本次测试可以在TimedTest构造函数中增加第三个参数,举例如下:

    long maxElapsedTime = 1000;

    Test testCase = new ExampleTestCase("testOneSecondResponse");

    Test timedTest = new TimedTest(testCase, maxElapsedTime, false);

    以下代码创建了一个执行时间的测试,用来测试被定义在单元测试ExampleTestCase.testOneSecondResponse()方法所代表的功能执行的时间。

    执行效率测试举例
    import com.clarkware.junitperf.*;

    import junit.framework.Test;

    public class ExampleTimedTest {

      public static Test suite() {

        long maxElapsedTime = 1000;

        Test testCase = new ExampleTestCase("testOneSecondResponse");

        Test timedTest = new TimedTest(testCase, maxElapsedTime);

        return timedTest;

      }

      public static void main(String[] args) {

        junit.textui.TestRunner.run(suite());

      }

    }

    测试的粒度决定于JUnit的测试用例,并被JUnitPerf所使用,因此有一定的局限性。最终获得的执行时间为测试用例中testXXX()方法的执行时间,包括setUp(), testXXX(), 和tearDown()方法的执行时间。执行测试套件的时间包含测试套件中所有测试示例的setUp(), testXXX(), 和tearDown()方法的执行时间。所以,预期的时间还应该依照set-up和tear-down的执行时间来制定(把这部分时间也考虑进去)。

  • LoadTest

    LoadTest用来仿效多个用户并发执行多次来进行测试。

    LoadTest最简单的构造函数只有两个参数,测试用例和用户数,默认情况下该测试只迭代一次。

    例如,创建一个10用户并发执行一次ExampleTestCase.testOneSecondResponse()方法:

    int users = 10;

    Test testCase = new ExampleTestCase("testOneSecondResponse");

    Test loadTest = new LoadTest(testCase, users);

    负载测试过程也可以指定一个额外的计数器实例用来指定用户并发执行之间的延迟时间。ConstantTimer类构造函数包含一个常量参数,用来指定延迟时间,如果指定为0则表示所有的用户同时开始。RandomTimer类可以构造出随机的延迟时间。

    例如:创建一个负载测试,10个并发用户各执行一次ExampleTestCase.testOneSecondResponse()方法,各个用户之间延迟1秒钟执行。

    int users = 10;

    Timer timer = new ConstantTimer(1000);

    Test testCase = new ExampleTestCase("testOneSecondResponse");

    Test loadTest = new LoadTest(testCase, users, timer);

    为了仿效并发用户以指定迭代次数执行测试,LoadTest类构造函数包含了RepeatedTest参数。这样就可以为每个测试用例指定迭代次数了。

    例如:创建一个负载测试,10个并发用户,每个用户迭代执行ExampleTestCase.testOneSecondResponse()方法20次,每个并发用户之间延迟1秒。

    int users = 10;

    int iterations = 20;

    Timer timer = new ConstantTimer(1000);

    Test testCase = new ExampleTestCase("testOneSecondResponse");

    Test repeatedTest = new RepeatedTest(testCase, iterations);

    Test loadTest = new LoadTest(repeatedTest, users, timer);

    或者这样来写:

    int users = 10;

    int iterations = 20;

    Timer timer = new ConstantTimer(1000);

    Test testCase = new ExampleTestCase("testOneSecondResponse");

    Test loadTest = new LoadTest(testCase, users, iterations, timer);

    如果负载测试要求测试在setUp()方法中包含特殊的测试状态,那么就应该使用TestFactory类来确保每个并发用户线程使用一个本地线程测试实例。例如创建一个10用户并发的测试,每个用户运行ExampleStatefulTest类的一个本地线程,可这样来写:

    int users = 10;

    Test factory = new TestFactory(ExampleStatefulTest.class);

    Test loadTest = new LoadTest(factory, users);

    如果测试其中的某一个方法,可以这样:

    int users = 10;

    Test factory = new TestMethodFactory(ExampleStatefulTest.class, "testSomething");

    Test loadTest = new LoadTest(factory, users);

    以下的例子是测试单元测试ExampleTestCase.testOneSecondResponse()方法对应的功能的一个负载测试,用来测试该功能的执行效率。其中有10个并发用户,无延迟,每个用户只运行一次。LoadTest本身使用了TimedTest来得到在负载情况下ExampleTestCase.testOneSecondResponse()方法的实际运行能力。如果全部的执行时间超过了1.5秒则视为不通过。10个并发处理在1.5秒通过才算通过。

    负载下承受能力测试举例

    import com.clarkware.junitperf.*;

    import junit.framework.Test;

    public class ExampleThroughputUnderLoadTest {

      public static Test suite() {

        int maxUsers = 10;

        long maxElapsedTime = 1500;

        Test testCase = new ExampleTestCase("testOneSecondResponse");

        Test loadTest = new LoadTest(testCase, maxUsers);

        Test timedTest = new TimedTest(loadTest, maxElapsedTime);

        return timedTest;

      }

      public static void main(String[] args) {

        junit.textui.TestRunner.run(suite());

      }

}

在下面的例子中,测试被颠倒过来了,TimedTest度量ExampleTestCase.testOneSecondResponse()方法的执行时间。然后LoadTest中嵌套了TimedTest来仿效10个并发用户执行ExampleTestCase.testOneSecondResponse()方法。如果某个用户的执行时间超过了1秒则视为不通过。

负载下响应时间测试举例

import com.clarkware.junitperf.*;

import junit.framework.Test;

public class ExampleResponseTimeUnderLoadTest {

  public static Test suite() {

    int maxUsers = 10;

    long maxElapsedTime = 1000;

    Test testCase = new ExampleTestCase("testOneSecondResponse");

    Test timedTest = new TimedTest(testCase, maxElapsedTime);

    Test loadTest = new LoadTest(timedTest, maxUsers);

    return loadTest;

  }

  public static void main(String[] args) {

    junit.textui.TestRunner.run(suite());

  }

}

性能测试套件

下面的测试用例例子中把ExampleTimedTest和ExampleLoadTest结合在一个测试套件中,这样就可以自动地执行所有相关的性能测试了:Example Performance Test Suite

import junit.framework.Test;

import junit.framework.TestSuite;

public class ExamplePerfTestSuite {

  public static Test suite() {

    TestSuite suite = new TestSuite();

    suite.addTest(ExampleTimedTest.suite());

    suite.addTest(ExampleLoadTest.suite());

    return suite;

  }

  public static void main(String[] args) {

    junit.textui.TestRunner.run(suite());

  }

}

编写有效的JUnitPerf测试

Timed Tests

Waiting Timed Tests

默认情况下TimedTest测试中如果实际测试时间超过了预期时间则继续执行JUnit的测试。这种waiting timed test总是允许JUnit测试累积所有的测试结果,直到测试完成并且检查完所有的测试结果。

如果测试执行中等待测试完毕的用例直接或间接地派生多个线程,那么此次测试只有等到所有的线程执行完毕才会返回到timed test中。另外一方面该测试将无限期地等待。一般来说,单元测试应该等待所有派生的线程执行完毕,例如使用Thread.join()方法,以便准确地判断结果。

Non-Waiting Timed Tests

此外,TimedTest还提供了一个构造方法,当实际时间超过预期时间时立即表示未通过。这种类型的测试如果执行时间超过了预期的最大时间则不等待测试继续执行完毕。这种类型的测试比上一种方式更加有效,根据需要这种测试可节约时间,将不再等待程序执行并且立即标识测试未通过。然而,跟上面一种类型不同的是,这种类型的测试如果中间有测试不通过的话就不继续执行后面的测试了。

Load Tests

Non-Atomic Load Tests

默认情况下,如果LoadTest扩展出来的测试直接或间接地派生线程,它不会强制这种线程并发执行(正如在事务中定义的一样)。这种类型的测试假设它扩展的测试在当返回控制时交互地完成。例如如果扩展测试的派生线程和控制返回没有等待派生进程执行完毕,那么扩展测试就假定为一次性地完成了。

而一般来讲,单元测试中为了准确地判断结果,应该等待派生的线程也执行完毕,例如使用Thread.join()方法然而在某些情况下并不是一定要这样的。例如,对于EJB分布式的查询结果,应用服务器可能派生一个新的线程去处理这个请求。如果新的线程在同一个线程组中运行decorated测试(默认情况),那么一个非原子的压力测试仅仅等待压力测试直接派生的线程执行完毕而新生成的线程则会被忽略掉。

总之,非原子压力测试仅仅等待压力测试中直接派生的线程执行完毕来模仿多个并发用户。

Atomic Load Tests

如果多个线程规定一个decorated测试成功地执行,这就意味着只有所有decorated测试中的线程执行完毕这个decorated测试才被认为是完成了。可以使用setEnforceTestAtomicity(true)来强迫执行这种测试()。这将有效地促使这种测试等待属于decorated测试的线程组的所有线程执行完毕。原子性压力测试也会把任何过早退出的线程当成是失败。如果一个线程突然崩溃,那么属于同一线程组的其他线程就会立即停止执行。

如果decorated测试派生的线程属于同一个线程组,默认情况下线程执行decorated 测试,这样原子压力测试将无限期地等待派生的线程执行完毕。

总之,原子压力测试将等待所有属于同一线程组的线程执行完毕,压力测试直接派生的线程,来模仿多个用户并发。

局限性

JUnitPerf已知有以下缺陷:

  • TimedTest返回的时间是测试用例的testXXX()方法的时间,包括setUp(), testXXX()和 tearDown()三个方法的总时间,这是任何测试实例中所能提供的最小的测试粒度。因此期望的时间也应该考虑set-up 和tear-down的运行时间。(译者注:或者可以自己在JUnit测试用例使用System.currentTimeMillis()方法来计算某个步骤的执行时间)
  • JUnitPerf并不是一个完整的压力和性能测试工具,并且它也不会用来取代其它类似的工具。它仅仅用来编写本地的单元性能测试来帮助开发人员做好重构。
  • The performance of your tests can degrade significantly if too many concurrent users are cooperating in a load test. The actual threshold number is JVM specific.
  • 在压力测试中如果有太多的用户并发运行则测试情况会越来越糟。应该参照JVM的规范来指定用户数。
 

版权所有:UML软件工程组织