`
fantaxy025025
  • 浏览: 1279007 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
阅读更多

 

JUnit是一种典型的工具包:如果遵循它最初的设计规范加以使用,JUnit可以帮助开发人员建立起一组相当健壮的测试;反之,它可能只会给你的 项目带来一团乱麻。本文将列出一些有用的技巧,它们将帮助你避免让那团乱麻入侵你的项目。这些技巧有时候甚至会自相矛盾,但这是不可避免的。以我的经验来 看,软件开发几乎没有什么又快又好的规则,而那些自称是法则的东西大多是会有些误导的吧。

本文中我们会实现两种对开发人员有用的工具类:

  • 从文件系统中取得类的集合,并自动生成测试套件
  • 一个可以更好地支持多线程测试的测试类

在做单元测试的时候,许多开发团队总想自己去建立一套测试框架来完成单元测试的任务。而开源的JUnit提供了一套很好的单元测试框架,从而扼杀了 许多这种建立一次性测试框架的想法。如果把JUnit作为整个开发项目的一部分去完成,它将发挥出自己最大的能量,因为开发人员可以使用JUnit更加简 单地撰写并执行测试用例。那么,我们该如何使用JUnit呢?

 

不要使用构造器来初始化你的测试用例

在构造器里初始化你的测试用例可能并不是一个好主意。
考虑一下:

public class SomeTest extends TestCase
   public SomeTest (String testName) {
       super (testName);
       // Perform test set-up
   }
}

想象一下,当程序在执行初始化的时候,初始化部分的代码抛出了一个IllegalSateException的异常。而当JUnit抓住这个异常以 后,它会抛出一个AssertionFailedError,接着它会告诉你无法建立此测试类的实例。以下是一个此类异常消息的例子:

junit.framework.AssertionFailedError: Cannot instantiate test case: test1 at
junit.framework.Assert.fail(Assert.java:143) at
junit.framework.TestSuite.runTest(TestSuite.java:178) at
junit.framework.TestCase.runBare(TestCase.java:129) at
junit.framework.TestResult.protect(TestResult.java:100) at
junit.framework.TestResult.runProtected(TestResult.java:117) at
junit.framework.TestResult.run(TestResult.java:103) at
junit.framework.TestCase.run(TestCase.java:120) at
junit.framework.TestSuite.run(TestSuite.java, Compiled Code) at
junit.ui.TestRunner2.run(TestRunner.java:429)

 

这堆消息并没有提供给我们更多我们想要的信息,它只是在说无法建立这个类的实例。它没法指出是哪行代码抛出了这个异常,信息的缺失让开发人员很难从中找到异常的根源。
如果不在构造器里初始化测试数据,而是去重写setUp()这个方法,那你就能准确地定位任何在setUp()方法中抛出的异常。如果和先前的异常消息比较一下:

java.lang.IllegalStateException: Oops at bp.DTC.setUp(DTC.java:34) at
junit.framework.TestCase.runBare(TestCase.java:127) at
junit.framework.TestResult.protect(TestResult.java:100) at
junit.framework.TestResult.runProtected(TestResult.java:117) at
junit.framework.TestResult.run(TestResult.java:103)
...

这样的消息提供了更多的信息,你能知道这是一个IllegalStateException异常,而且也知道是从哪一行抛出来的。这样一来,找到并修复测试用例初始化时的错误就简单多了。

 

不要假定测试用例执行的顺序

你不该假

定测试用例会以任何特殊的顺序执行。看看以下这段代码:

public class SomeTestCase extends TestCase {

    public SomeTestCase (String testName) {
       super (testName);
   }
   public void testDoThisFirst () {
   ...
   }
   public void testDoThisSecond () {
   }
}

由于JUnit使用了Reflection机制,它就不能确保测试用例会以任何一种特定的顺序来执行。在不同平台、不同的JVM上可能会有不同的结 果,除非你在设计测试用例时就规定好了某种顺序。其实,不依赖于某种顺序的测试用例会更加健壮,因为打乱这些用例的顺序也不会使它们互相影响。如果用例依 赖于某种顺序,那么即使你只是做了很小的改动,也将会很难定位缺陷发生的位置。

只有在少数情况下,依赖于某种顺序的测试用例才有意义。比如,当后一个用例的执行依赖于前一个用例留下的数据时,按照顺序执行的测试用例将会更加高效。这时,你需要使用一个静态的suite()方法来确保用例的执行顺序,比如这样:

public static Test suite() {
   suite.addTest(new SomeTestCase ("testDoThisFirst";));
   suite.addTest(new SomeTestCase ("testDoThisSecond";));
   return suite;
}

JUnit的API文档中并没有保证测试用例会以一定的顺序执行,因为JUnit使用了一个Vector来储存这些测试。但是,如果你使用了类似上面的代码,你就可以确保这些用例执行的顺序,因为它们已经被加到一个测试套件中了。

 

避免测试带来的副作用

测试用例的副作用会带来两个问题:

  • 如果它们改变了外部的数据,其它依赖于这些数据的用例就会受到影响
  • 你将不得不手工修改外部数据,才能重复进行测试

在第一种情况中,一个测试用例可能会执行通过。但如果把这个测试用例加到某一组测试套件中,它就可能会导致套件中的其他用例执行失败。而且你会很难调试这些错误,用例失败的根源可能离显示出错的地方很远。

在第二种情况中,测试用例可能会去修改某些系统状态或者数据,如果不手工把它们改回来,第二次执行该用例可能就会失败了,就比如测试用例所测的函数 要去删除数据库里的某些数据。所以,当你不得不在测试中加入一些手工操作时,请三思而后行。因为首先,这些手工操作需要被记录到文档中;第二,这些用例就 再也不是“无人值守”的测试了,你再也不能把这些测试放到晚上执行,而自己却在一边睡大觉了。

 

写测试用例时请重写setUp()和tearDown()方法

考虑一下如下的代码:

public class SomeTestCase extends AnotherTestCase {
   // A connection to a database
   private Database theDatabase;
   public SomeTestCase (String testName) {
       super (testName);
   }
   public void testFeatureX () {
   ...
   }
   public void setUp () {
       // Clear out the database
       theDatabase.clear ();
   }
}

你能发现以上这段代码的错误吗?setup()方法应该调用super.setUp() 方法来确保AnotherTestCase所定义的环境已经被初始化了。(注意:这个在Junit4中已经不需要了) 当然,有一个例外:如果你所设计的基类中,最多只有静态的测试数据,就不会有什么问题了。

 

在读取测试数据是避免使用绝对路径

测试经常需要从文件系统的某个路径中读取测试数据,考虑一下以下这段代码:

public void setUp () {
   FileInputStream inp ("C:\\TestData\\dataSet1.dat");
   ...
}

如果要使用以上这段代码,就必须确保数据在C:\TestData这个位置。在两种情况下,这种假设可能会出问题:

  • 在测试人员电脑的C盘中可能没有足够的磁盘空间,所以只能把数据保存在另一个磁盘上
  • 测试可能需要在另一个平台上运行,比如Unix

我可能以使用以下方法:

public void setUp () {
   FileInputStream inp ("dataSet1.dat");
   ...
}

如果想要使用这种方法,必须确保程序运行时的目录和测试数据的目录相一致。如果有几个不同的测试用例需要有这种限制,那除了改变当前的测试数据的目录就没有办法把这些测试用例整合在一个测试套件中了。

为了解决这个问题,可以使用Class.getResource()或者Class.getResourceAsStream来取得数据集。使用这种方式就意味着测试数据的路径是于程序运行时目录的相对路径。

如果可能的话,测试数据应该和源代码一起被保存在一个配置管理系统中。然而,如果你使用了如前所述的机制,你需要另写一个脚本把这些测试数据从配置 管理系统复制到测试环境的运行目录中。另一种方法是把这些测试数据直接存储在测试环境中。如果使用这种方法,你需要一个与物理地址无关的机制来指定测试数 据。你可以使用类,你可以写如下的代码,来映射一个类到一个指定的目录中:

InputStream inp = SourceResourceLoader.getResourceAsStream (this.getClass (), "dataSet1.dat");

这样一来,你只要决定如何来做这些类与测试数据之间的映射。你可以用一个系统参数来指定该资源树的根目录。然后类的完整的包名可以用来确定测试数据 所在目录,测试数据便会从那个目录载入进来。对于Unix和Windows Nt两种不同的操作系统来说,映射的方式显得更加直截了当,这样就可以用“.”来代替File.separatorChar了。

 

把测试和测试对象放在同一目录下

如果测试类和被测试类的代码放在一个目录下,那么在一次build时,会同时编译测试和被测试类。这样一来就促使你在开发过程中保持测试用例和被测试方法的同步。事实上,如果单元测试不能和测试对象同步,那它就因过期而根本毫无用处。

 

给测试用例起些好名字

给测试起些如TestClassUnderTest的名字。比如说,如果一个测试类是用来测试MessageLog这个类的,就该叫TestMessageLog。那样哪个测试对应哪个测试对象会更加明了。测试类中的每个用例应该描述它是在测些什么:

  • testLoggingEmptyMessage()
  • testLoggingNullMessage()
  • testLoggingWarningMessage()
  • testLoggingErrorMessage()

合适的名字能帮助开发人员更好地理解每个测试用例的意图。

确保测试结果与时间无关
如果有可能,避免使用一些可能会过期的数据;这样的数据需要手工或用程序来更新。通常来说用测试去改变测试对象的一些初始化属性都是很简单的,就好象改变它“今天”的概念。这样测试用例就可以顺利运行,而不用去更新测试数据了。

 

考虑一下地区因素

考虑一下,如果在测试中用到日期。创建日期的方法可能是这样的:

Date date = DateFormat.getInstance ().parse ("dd/mm/yyyy");

很遗憾,那样的代码可能在另外一台在不同地区的机器上就不能正常运作。因此,这样写可能会更好:

Calendar cal = Calendar.getInstance ();
Cal.set (yyyy, mm-1, dd);
Date date = Calendar.getTime ();

第二种方法在运行换将地区不同的情况下更有兼容性。

 

使用JUnit的assert和fail方法,以及异常处理,可以让你的测试代码看上去更干净

许多JUnit的初学者喜欢用看上去可能更精巧的try和catch来捕捉异常,然后在catch中标记测试失败了。以下是一个例子:

public void exampleTest () {
   try {
       // do some test
   } catch (SomeApplicationException e) {
       fail ("Caught SomeApplicationException exception");
   }
}

实际上,JUnit会自动捕捉异常。它会把那些你没在测试代码中捕捉到的异常识别为错误,也就是说以上的实例中有许多多余的代码。
以下是一段更简单的代码,但它能完成同样的事:

public void exampleTest () throws SomeApplicationException {
   // do some test
}

在这个例子中,多余的代码被去除了,这样一来代码就更容易阅读和维护了。
广泛地使用多种assert方法来表达你的意图,而不是像这样:

assert (creds == 3);

应该像这样:

assertEquals ("The number of credentials should be 3", 3, creds);

以上这个例子对要读这段代码的人会更有帮助。而且如果这个断言失败了,它能提供给测试人员更多的信息。JUnit也支持浮点数的比较:

assertEquals ("some message", result, expected, delta);

当你在比较浮点数的时候,这个方法能帮助你更加快速地计算出实际结果和期望结果之间的差值。
使用assertSame()测试的是两个引用是不是指向同一个对象。使用assertEquals()来测试两个对象是不是完全相等。

 

在javadoc中编写测试文档

用word来写测试计划通常会有很多问题,而且写起来也很单调。这些用word敲出来的文档还需要和单元测试保持同步,这样就让测试流程更加复杂。如果可能的话,用javadoc来写测试文档会是一个较好的方案,这样也能确保所有的测试计划和用例都储存在一个地方。

 

尽量避免视觉检查的用例

在测试servlets、用户界面、或者其他一些会有比较复杂的输出的程序时,通常都会用到视觉检查的方法。视觉检查——就是由人类来检查输出的数 据正确与否——需要耐心,抓取大量信息的能力,以及对细节的专注力,而这些优点在大多数人身上都是很难找到的。以下这些基本的技术会帮助你在进行测试时尽 量避免视觉检查。

Swing

当你在测试一个基于Swing的用户界面时,你可以写一些用例:

  • 所有的component都呆在正确的panel里
  • 正确配置了样式管理项
  • 文本组件的字体正确

你可以在这里 找到更多深入的相关的例子。

XML

当你在测试一些处理XML的类时,可能会花上很多时间用眼睛去查看XML文件的DOM是否和预期中的完全一致。其实你可以事先定义一个正确的DOM,然后和真正的输出做一个文本对比 即可。

Servlets

你可以有许多方法来测试Servlets。你可以写一个虚拟的servlet框架并在测试之前事先配置好。在这个框架中需要包含正常环境中的一部分类。通过在虚拟的框架中来调用这些类来完成测试。

比如:

  • 可以继承HttpServletRequest来写测试类,指定header,方法,路径信息和其他一些数据。
  • 可以继承HttpServletResponse来写测试类,允许按时返回一个被测试类所期待的输出。

一个更简单的方案是使用HttpUnit来测试servlets。HttpUnit提供一个DOM视图来显示发送请求的结果,这样就能比较简单地来比较真实的数据和预期的结果。

你可以用很多方法来避免视觉检查。然而,有些时候,使用视觉检查 或者是某些专门的测试工具 会来得更省。比如说,用JUnit来测试用户界面的动态行 为尽管是可能的,但是会很复杂。从数量庞大的记录/回放测试工具中挑一个买回来用用可能是个更好的主意,或者,去做一些少量的视觉检查也不无不可。然而, 这不意味着在通常情况下,你可以忽视别用视觉检查的忠告。

执行整个系统上的所有测试不应该花上几个小时的时间。实际情况是,开发人员会更喜欢去执行那些跑起来很快的测试。如果不经常去执行系统上的所有测试,确保系统在某些更改后仍能正常运行就会非常困难。缺陷会悄无声息地潜入系统。

 

使用Reflection驱动的JUnit的API

允许TestSuite使用reflection,以此来减少维护时间。Reflection可以使你不必为任何新加的测试用例去更新整个suite()。

 

建一个用来测试整个系统的测试用例

建一个针对整个系统的测试用例相当有必要。如果有一个测试用例能覆盖整个系统,开发人员就能通过那个用例来测试自己的改动是否会影响到这系统中的任 何细小之处。如果有这样一个测试,我们就能及早发现那些代码更新所带来的不易察觉的缺陷。如果没有这么一个测试,开发人员通常只会愿意去测试他们刚改动过 的那个类。而且,执行所有的测试就会是件谁都不愿干的痛苦的体力活。

如果我们建了一个针对整个系统的测试,它将包括所有的已定义的测试用例。可以用这些测试用例来定义suite()的方法,这样一来,针对整个系统的 一个测试套件就建成了。如果你有许多测试用例,建立这么一个测试套件可能会花掉很多时间。另外,如果有新的测试用例,或者已定义的那些测试用例需要重命名 或者用不到了,那你也必须去更新整个测试套件。如果你不想手动去建立和维护这样一个测试套件,而是想让所有的测试用例自己就能生成这么一个套件。这些被引 用的测试用例需要符合以下条件:

  • 它不能是自行载入的,因为那样会造成递归。所以我们需要确保测试用例是不可载入的。
  • 在类中不能载入那些从TestCase继承下来的类,也不能直接运行。
  • 需要区分单元测试和其他的测试,比如载入或压力测试。别让不同的测试在一次执行测试的时候全部跑完。
  • 接下来要介绍的测试工具类会递归地去查找当前目录下的所有文件,找到那些测试用例并把它们加到测试套件中来。

我们可以用Java的继承方式去定义我们的测试用例究竟属于哪个测试类别。我们可以让我们的那些测试用例分别从 UnitTest,StressTest,LoadTest等等继承过来。不过,这样一来,那些测试用例就很难在不同的测试类别中重用,因为测试的类别是 在很靠近基类的地方就定义下来的;它应该是在每个子类中才定义的。另一种方法,我们可以用这么一个字段来区别不同的测试类别:public final String TEST_ALL_TEST_TYPE。这样一来,只有在这个字段和启动自动化测试时配置的那个测试类别参数相匹配的时候,这个测试类的用例才会被执行。 我们可以用三个类来实现这种方法:

  • ClassFinder可以递归地去寻找当前目录下的所有类。所有被找到的类都载入到运行环境中,并拿到所有完整的类名。把这些类名加到一个列表中,以待在之后的过程中载入它们。
  • TestCaseLoader会去载入刚才拿到的那个列表中的类,并判断这是不是一个测试用例。如果是的话,再加入到另一个测试用例的列表中。
  • TestAll是从TestCase继承过来的一个子类,并实现了suite()的方法,来载入TestCaseLoader所生成的测试用例列表中的那些类。

让我们来具体看看这些类的实现。

ClassFinder

ClassFinder是用来查找系统中的那些类的(包括测试对象和测试用例),它们都被储存在一个目录下。ClassFinder会找到目录下的所有类,并在之后的测试过程中使用。ClassFinder具体实现的第一部分如下:

public class ClassFinder {
   // The cumulative list of classes found.
   final private Vector classNameList = new Vector ();
   /**
     * Find all classes stored in classfiles in classPathRoot
     * Inner classes are not supported.
     */
   public ClassFinder(final File classPathRoot) throws IOException {
       findAndStoreTestClasses (classPathRoot);
   }
   /**
     * Recursive method that adds all class names related to classfiles it finds in
     * the currentDirectory (and below).
     */
   private void findAndStoreTestClasses (final File currentDirectory) throws IOException {
       String files[] = currentDirectory.list();
       for(int i = 0;i < files.length;i++) {
           File file = new File(currentDirectory, files[i]);
           String fileBase = file.getName ();
           int idx = fileBase.indexOf(".class");
           final int CLASS_EXTENSION_LENGTH = 6;
           if(idx != -1 && (fileBase.length() - idx) == CLASS_EXTENSION_LENGTH) {

... ... ... ...

在以上的代码中,我们迭代查找了目录下的所有文件。如果该文件的扩展名为“.class”,我们就把该文件的类名储存起来,就像这样:

JcfClassInputStream inputStream = new JcfClassInputStream(new FileInputStream (file));
JcfClassFile classFile = new JcfClassFile (inputStream);
System.out.println ("Processing: " + classFile.getFullName ().replace ('/','.'));
classNameList.add (classFile.getFullName ().replace ('/','.'));

 

这段代码用到了JCF包来载入类文件,并得到这些类文件的类名。JCF包是一个用来载入和检查类文件的工具包。(更多信息点 )。JCF包可以帮我们得到所有类的完整类名。我们虽然可以从文件名来臆测类名,但是如果在某种环境下并不把类名当文件名保存起来事情就不好办了。如果是内部类的话也不能这么实现:

最后,我们检查文件是否为目录(请看下面的代码段)。如果是,我们就递归查找到该目录下,这样我就能拿到一个目录下的所有类了。

           } else if(file.isDirectory()) {
               findAndStoreTestClasses (file);
           }
       }
   }
   /**
     * Return an iterator over the collection of classnames (Strings)
     */

    public Iterator getClasses () {
       return classNameList.iterator ();
   }
}

TestCaseLoader

TestCaseLoader从刚才ClassFinder得到的那些类中得到测试类。以下的代码段展示了一些找到这些测试用例的上层代码:

public class TestCaseLoader {
   final private Vector classList = new Vector ();
   final private String requiredType;
   /**
     * Adds testCaseClass to the list of classdes
     * if the class is a test case we wish to load. Calls
     * shouldLoadTestCase () to determine that.
     */
   private void addClassIfTestCase (final Class testCaseClass) {
       if (shouldAddTestCase (testCaseClass)) {
           classList.add (testCaseClass);
       }
   }
   /**
     * Determine if we should load this test case. Calls isATestCaseOfTheCorrectType
     * to determine if the test case should be
     * added to the class list.
     */
   private boolean shouldAddTestCase (final Class testCaseClass) {
       return isATestCaseOfTheCorrectType (testCaseClass);
   }

你可以在下面找到一个叫isATestCaseOfTheCorrectType()的方法。它会去检查列表中的每个类:

  • 检查它是否是从TestCase继承来的。如果不是,那它就不是测试用例。
  • 检查该类中的TEST_ALL_TEST_TYPE和该类成员变量requiredType是否一致。

 

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

   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拖慢了你测试的进程。但是,在几周以后,你会开始去改善现有的代码。接着你会继续执行测试,找到新的缺陷,并修复 它们。你会对你的编程能力更有信心,并真切感受到单元测试的巨大价值。

 

 

-----------------------------------------------------------------------------------------------------

摘自:http://imapollo.blogbus.com/logs/42992064.html

         http://imapollo.blogbus.com/logs/42993379.html

         http://imapollo.blogbus.com/logs/42993462.html

-----------------------------------------------------------------------------------------------------

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

-----------------------------------------------------------------------------------------------------

分享到:
评论

相关推荐

    JUnit 测试用例的20条最佳实践(中文版)

    原文地址:https://dev-cheats.com/java/unit-testing-best-practices-junit-reference-guide.html 这个资源是把文章整理成PDF,方便本地查看。

    软件测试与Junit实践

    《软件测试与Junit实践》一书主要针对的是软件开发中的一个重要环节——测试,特别是针对Java编程语言的单元测试工具...通过阅读书中包含的pdg文件,读者可以深入了解各种测试技巧和最佳实践,提高自己的测试技能。

    Junit的一份资料

    Junit的实施涉及到多个方面,包括测试概念、JUnit框架的使用、最佳实践以及与集成开发环境(IDE)的集成。 测试的概念是软件开发中的关键环节,它旨在发现和修复错误,确保软件的质量。白盒测试,也称为结构测试,...

    单元测试JUnit学习专题

    #### 六、JUnit最佳实践 **测试驱动开发(TDD)**:TDD是一种开发方法论,强调在编写功能代码之前先编写测试代码。 - **编写测试**:首先编写失败的测试案例。 - **编写代码**:然后编写能够通过测试的代码。 - **...

    Junit的一份资料.doc

    JUnit 最佳实践包括: - **编写独立的测试**:每个测试用例应独立于其他测试,避免相互影响。 - **测试覆盖率**:确保测试覆盖了代码的各个分支和边界条件。 - **快速反馈**:测试应迅速执行,以便在开发过程中频繁...

    JUnit 使用的最佳实践

    本篇文章将详细阐述 JUnit 使用的最佳实践,帮助开发者更好地理解和应用 JUnit。 首先,理解 JUnit 的基本使用规范至关重要。每个测试方法都需使用 `@Test` 注解进行标记,且方法必须是 public void 类型,不接受...

    软件测试与Junit实践.rar

    五、最佳实践 1. 单元测试原则:保持测试用例独立,避免依赖外部环境;每个测试用例应快速执行,便于频繁运行。 2. 测试驱动开发(TDD):先写测试用例,再编写实现代码,以测试驱动开发流程,有助于提升代码质量。 ...

    软件测试与junit实践Java测试

    在软件开发过程中,测试是确保产品质量的关键环节。Java测试领域中,JUnit是一个广泛使用的单元测试框架,它使得开发者能够编写可重复执行、...在实际项目中结合最佳实践,如TDD,可以进一步提升团队的整体开发水平。

    JUnit best practices

    ### JUnit最佳实践详解 #### 一、单元测试单一对象 **知识点概述:** 单元测试的一个核心原则就是针对单个对象进行测试。这意味着每个测试用例都应该独立地检验程序中的一个对象或组件的行为,以便在出现问题时...

    JUnit in action JUnit Recipies

    最后,《软件测试与Junit实践_0.zip》可能是一份关于软件测试实践的文档,可能包含Junit在实际项目中的应用案例和最佳实践。 总的来说,这两本书是学习和提升Java单元测试技能的宝贵资源,无论是初学者还是经验丰富...

    JUnit in Action(JUnit经典书籍)中文+英文

    本书详尽地探讨了如何有效地使用JUnit进行软件测试,包括基础概念、高级特性和最佳实践,旨在帮助读者提升软件质量,降低维护成本,以及通过自动化测试提升开发效率。 在书中,作者首先介绍了JUnit的基本概念,包括...

    junit测试_java_JUnit_JUnit测试_

    在"junit测试.txt"这个文件中,可能包含了关于如何使用JUnit进行测试的详细步骤、示例代码和最佳实践。例如,文件可能会讲解如何设置一个基本的JUnit测试类,通常这个类会继承自`junit.framework.TestCase`(对于较...

    junit-4.12.jar下载

    JUnit是Java编程语言中最常用的单元测试框架之一,它允许开发者编写可执行的测试用例来验证代码的功能。这里我们关注的是`junit-4.12.jar`版本...同时,了解不同版本之间的差异也有助于理解JUnit的发展历程和最佳实践。

    junit4 jar完整包

    为了更好地利用JUnit4进行单元测试,我们需要遵循一些最佳实践,如编写独立的测试用例,避免过度复杂的测试,以及充分覆盖代码的各种边界情况。同时,良好的测试覆盖率可以帮助我们发现潜在的代码问题,提高软件质量...

    软件测试与junit实践1详细

    六、最佳实践 1. 测试驱动开发(TDD):先编写测试,再编写生产代码,使代码质量更高。 2. 单元测试的独立性:每个测试方法应独立运行,不受其他测试影响。 3. 充分覆盖:确保测试用例覆盖所有可能的代码路径和边界...

    junit实战第二版

    本段所提及的《JUnit实战第二版》是一本深入讲解JUnit框架的书籍,旨在帮助读者掌握JUnit的各种特性和最佳实践。书籍的结构分为多个章节,其中第一章节和第二章节分别介绍了JUnit的基本使用和深入探索。 在第一章...

Global site tag (gtag.js) - Google Analytics