原文:http://www.javaworld.com/jw-12-2000/jw-1221-junit.html

以下是实现该功能的代码:

   private boolean isATestCaseOfTheCorrectType (final Class testCaseClass) {
       boolean isOfTheCorrectType = false;
       if (TestCase.class.isAssignableFrom(testCaseClass)) {
           try {
               Field testAllIgnoreThisField = testCaseClass.getDeclaredField("TEST_ALL_TEST_TYPE");
               final int EXPECTED_MODIFIERS = Modifier.STATIC | Modifier.PUBLIC | Modifier.FINAL;
               if (((testAllIgnoreThisField.getModifiers() & EXPECTED_MODIFIERS) != EXPECTED_MODIFIERS) ||
                   (testAllIgnoreThisField.getType() != String.class)) {
                   throw new IllegalArgumentException ("TEST_ALL_TEST_TYPE should be static private final String");
               }
               String testType = (String)testAllIgnoreThisField.get(testCaseClass);
               isOfTheCorrectType = requiredType.equals (testType);
           } catch (NoSuchFieldException e) {
           } catch (IllegalAccessException e) {
               throw new IllegalArgumentException ("The field " + testCaseClass.getName () + ".TEST_ALL_TEST_TYPE is not accessible.");
           }
       }
       return isOfTheCorrectType;
   }

接下来,loadTestCases()方法会去检查每个类名。如果该类是可载入的,那么loadTestCases()就会去载入它;如果该类是测试用例,而且是需要的测试类型,该方法则会把这个类加到测试用例列表中:

   public void loadTestCases (final Iterator classNamesIterator) {
       while (classNamesIterator.hasNext ()) {
           String className = (String)classNamesIterator.next ();
           try {
               Class candidateClass = Class.forName (className);
               addClassIfTestCase (candidateClass);
           } catch (ClassNotFoundException e) {
               System.err.println ("Cannot load class: " + className);
           }
       }
   }
   /**
     * Construct this instance. Load all the test cases possible that derive
     * from baseClass and cannot be ignored.
     * @param classNamesIterator An iterator over a collection of fully qualified class names
     */
   public TestCaseLoader(final String requiredType) {
       if (requiredType == null) throw new IllegalArgumentException ("requiredType is null");
       this.requiredType = requiredType;
   }
   /**
     * Obtain an iterator over the collection of test case classes loaded by loadTestCases
     */
   public Iterator getClasses () {
       return classList.iterator ();
   }

TestAll

TestAll把所有的东西聚合到一起。它用到了前面所提到的那些类来建立测试用例的列表。它把所有的这些用例都加到一个测试套件中,并给出一个suite()方法。所以结果就是:它会自动生成一个测试套件,其中自动解析了系统中所有的测试用例,随时随地都可以使用JUnit来执行。

public class TestAll extends TestCase {

addAllTest()方法使用TestCaseLoader方法来遍历载入所有的测试用例到测试套件中:

   private static int addAllTests(final TestSuite suite, final Iterator classIterator)
       throws java.io.IOException {
       int testClassCount = 0;
       while (classIterator.hasNext ()) {
           Class testCaseClass = (Class)classIterator.next ();
           suite.addTest (new TestSuite (testCaseClass));
           System.out.println ("Loaded test case: " + testCaseClass.getName ());
           testClassCount++;
       }
       return testClassCount;
   }

有了suite(),所有的测试用例都会被加到测试套件中,然后由JUnit来执行。它从系统参数中得到“class_root”参数:类所储存的根目录。用ClassFinder找到所有的类,然后用TestCaseLoader去载入需要的测试用例。然后把这些测试用例都拿来创建一个测试套件:

   public static Test suite()
       throws Throwable {
       try {
           String classRootString = System.getProperty("class_root");
           if (classRootString == null) throw new IllegalArgumentException ("System property class_root must be set.");
           String testType = System.getProperty("test_type");
           if (testType == null) throw new IllegalArgumentException ("System property test_type must be set.");
           File classRoot = new File(classRootString);
           ClassFinder classFinder = new ClassFinder (classRoot);
           TestCaseLoader testCaseLoader = new TestCaseLoader (testType);
           testCaseLoader.loadTestCases (classFinder.getClasses ());
           TestSuite suite = new TestSuite();
           int numberOfTests = addAllTests (suite, testCaseLoader.getClasses ());
           System.out.println("Number of test classes found: " + numberOfTests);
           return suite;
       } catch (Throwable t) {
           // This ensures we have extra information. Otherwise we get a "Could not invoke the suite method." message.
           t.printStackTrace ();
           throw t;
       }
   }
   /**
     * Basic constructor - called by the test runners.
     */
   public TestAll(String s) {
       super(s);
   }
}

如果想用这些类来测试驱动并测试整个系统,只需执行下面的命令(windows)

java -cp C:\project\classes;C:\junit3.2\junit.jar:C:\jcf\jcfutils.zip -Dclass_root=C:\project\classes -Dtest_type=UNIT junit.ui.TestRunner bp.TestAll

这行命令回去载入执行所有类型为UNIT,并保存在C:\porject\classes下的测试用例。

测试线程的安全性

你可能想通过测试来确保代码中多线程的安全性。但是事实证明用现存的JUnit3.2是很难做到这点的。你可以使用junit.extensions.ActiveTest来让测试用例在不同的线程中运行。但是,测试套件只会在run()返回结果时才会判断该测试用例已经结束了;所以,如果使用junit.extension.ActiveTest,多线程就不行了。定义一个ActiveTestSuite会非常困难;让我们来看一个更简单的解决方案:MultiThreadedTestCase。首先,我会秀一下MultiThreadedTestCase是怎么帮助到多线程程序的测试的。然后我会大概讲一下MultiThreadedTestCase的实现。

想要使用MultiThreadedTestCase,我们需要从该类继承并实现我们自己的测试类,而MultiThreadedTestCase也是从TestCase继承来的。它继承了一些标准的元素,其中包括类的申明,构造器,还有既然我们用上TestAll了,把测试类型也写吧。

public class MTTest extends MultiThreadedTestCase {
   /**
     * Basic constructor - called by the test runners.
     */
   public MTTest(String s) {
       super (s);
   }
   public static final String TEST_ALL_TEST_TYPE = "UNIT";

多线程测试用例需要生成一些线程来完成一些动作。我们需要去开启那些线程,一直等到执行完了,再把结果返回给JUnit——所有的这些都在下面的代码中实现了。这段代码很琐碎;这段代码会生成一些线程来完成不同的工作。在所有的线程完成操作以后,测试用例会去判断一些变量或者是后置条件来检验这个类的运行状态是否正确。

   public void testMTExample () {
       // Create 100 threads containing the test case.
       TestCaseRunnable tct [] = new TestCaseRunnable [100];
       for (int i = 0; i < tct.length; i++) {
           tct[i] = new TestCaseRunnable () {
               public void runTestCase () {
                   assert (true);
               }
           };
       }
       // Run the 100 threads, wait for them to complete and return the results to JUnit.
       runTestCaseRunnables (tct);
   }
}

现在我已经展示了如何使用MultiThreadedTestCase,再让我们看看它是如何实现的。首先,我们声明一个类,并新建一个数组,这个数组会维护将来测试中运行的那些线程:

public class MultiThreadedTestCase extends TestCase {
   /**
     * The threads that are executing.
     */
   private Thread threads[] = null;

下面的这段,testResult代表一个测试用例是否通过。我们重载了run()的方法,这样我们就能存储多线程中较晚结束的那个线程的测试结果。

   /**
     * The tests TestResult.
     */
   private TestResult testResult = null;
   /**
     * Simple constructor.
     */
   public MultiThreadedTestCase(final String s) {
       super(s);
   }
   /**
     * Override run so we can save the test result.
     */
   public void run(final TestResult result) {
       testResult = result;
       super.run(result);
       testResult = null;

runTestCaseRunnables()方法把所有可执行的测试用例分在不同的线程中执行。所有的线程会被一起创建,并在相同的时间开启运行。这个方法会等到每个线程运行结束并返回给它。

   protected void runTestCaseRunnables (final TestCaseRunnable[] runnables) {
       if (runnables == null) {
           throw new IllegalArgumentException("runnables is null");
       }
       threads = new Thread[runnables.length];
       for (int i = 0;i < threads.length;i++) {
           threads[i] = new Thread(runnables[i]);
       }
       for (int i = 0;i < threads.length;i++) {
           threads[i].start();
       }
       try {
           for(int i = 0;i < threads.length;i++) {
               threads[i].join();
           }
       } catch(InterruptedException ignore) {
           System.out.println("Thread join interrupted.");
       }
       threads = null;
   }

在线程中抓到的异常应该要传到测试结果中去。下面这段代码中的handleException()就做了这个:

   /**
     * Handle an exception. Since multiple threads won't have their
     * exceptions caught the threads must manually catch them and call
     * handleException().
     * @param t Exception to handle.*/
   private void handleException(final Throwable t) {
       synchronized(testResult) {
           if(t instanceof AssertionFailedError) {
               testResult.addFailure(this, (AssertionFailedError)t);
           } else {
               testResult.addError(this, t);
           }
       }
   }

最后我们为测试类中的每个线程都扩展了这个方法。目的是为了在运行环境中抓住每个线程的异常,并把它们传回给JUnit。这个类的实现如下:

   /**
     * A test case thread. Override runTestCase () and define
     * behaviour of test in there.*/
   protected abstract class TestCaseRunnable implements Runnable {
       /**
         * Override this to define the test*/
       public abstract void runTestCase()
           throws Throwable;
           /**
             * Run the test in an environment where
             * we can handle the exceptions generated by the test method.*/
       public void run() {
           try {
               runTestCase();
           } catch(Throwable t) /* Any other exception we handle and then we interrupt the other threads.*/ {
               handleException(t);
               interruptThreads();
           }
       }
   }
}

以上这段代码可以帮助开发多线程测试用例。它可以处理多线程中所抛出的异常并把它们返回给JUnit。JUnit这边只以为这些测试用例是以单线程的形式运行的。单元测试人员可以扩展这个类来完成多线程的测试,而不用花太多时间来做一些针对多线程处理的代码。

总结

使用JUnit开发健壮的测试需要开发人员多加实践(写测试用例也是一样)。这篇文章中介绍了一些可以帮助你提高测试有效性的技术。这些技术包括从如何避免基本错误到一些设计层面的问题。我也在此文中提到了一些基本的帮助你完成部分UI和网络应用测试的方法。我也展示了一个实现自动化测试套件的方法,而不用手动硬编码去维护一个测试套件的机制,还有一些开发多线程测试的方法。

JUnit是一个对Java程序进行单元测试的很棒的框架。最后,如果你刚开始使用JUnit,请坚持下去。在开始的几周,你可能从你的工作中得不到什么回报。而且你可能会感觉JUnit拖慢了你测试的进程。但是,在几周以后,你会开始去改善现有的代码。接着你会继续执行测试,找到新的缺陷,并修复它们。你会对你的编程能力更有信心,并真切感受到单元测试的巨大价值。

相关文章:JUnit最佳实践[译][一]JUnit最佳实践[译][二]

分享到:

    评论

  • JUnit的最大优点是将一批又一批的毕业生带入了J2EE的殿堂- -
    回复老鼠说:
    着实贡献不小
    2009-08-02 21:46:50