• 1. JUnit框架技术 软件部 汪伟
  • 2. 第一篇 什么是JUnit?如果您要对撰写的程式进行测试,该如何进行呢?传统的测试方式通常依赖于人工对输出结果的判断,缺少效率且通常难以组织,且针对单一程式通常要设计专门的测试程式,如果您是在撰写Java,您可以使用JUnit来为您提供有效率的测试。 什么是JUnit?在这边引述一下JUnit FAQ中的解释。 JUnit是一个开放源码的Java测试框架,它用来撰写与执行重复性的测试,它是用于单元测试框架的xUnit架构的实例.。
  • 3. 第一篇 什么是JUnit?JUnit包括以下的特性: 对预期结果的断言 对相同共享资料的测试装备 易于组织与执行测试的测试套件 图型与文字介面的测试器 JUnit最初是由Erich Gamma与Kent Beck撰写 英文原文: JUnit is an open source Java testing framework used to write and run repeatable tests. It is an instance of the xUnit architecture for unit testing frameworks.
  • 4. 第一篇 什么是JUnit?JUnit features include: Assertions for testing expected results Test fixtures for sharing common test data Test suites for easily organizing and running tests Graphical and textual test runners JUnit was originally written by Erich Gamma and Kent Beck.
  • 5. 第一篇 什么是JUnit?JUnit的用途是单元测试(Unit Test),它针对单一个套件(package)中的类别进行测试,它假设其它套件中的类别运作是正常的。另一个测试的主题是功能测试(Functional Test),它将整个系统当作一个黑盒子,测试时假设除了提供的介面操作之外(例如GUI或是Web介面操作),剩下的内部细节一无所知,在某些程度上来说,功能测试简单的说就是对成品的测试,对于一个中大型的系统来说,功能测试依赖一个团队,他们的目的就是找出系统的臭虫(bug),通常使用一些工具或是相关技术来进行测试。 JUnit的官方网站在:http://www.junit.org
  • 6. 第二篇 JUnit安装设定要安装JUnit,先至JUnit官网下载JUnit3.8.1.zip(当时最新版本),下载后解开压缩包,当中会含有junit.jar,将这个档案复制至您所要的资料夹中,然后设定CLASSPATH 例如代码: Set classpath=%classpath%;INSTALL_DIR\junit3\ junit.jar 如果是Windows 2000/XP,请在[系统属性/高级/环境变量]中设定[系统变量]中的CLASSPATH,如果没有就自行新增,如下所示:
  • 7. 第二篇 JUnit安装设定
  • 8. 第二篇 JUnit安装设定您可以在解开JUnit3.8.1.zip的目录下运行以下的测试范例,看看您的CLASSPATH是否设定正确: 文字模式测试范例: 代码: java junit.textui.TestRunner junit.samples.AllTests AWT图形模式测试范例: 代码: java junit.awtui.TestRunner junit.samples.AllTests
  • 9. 第二篇 JUnit安装设定SWING图形模式测试范例: 代码: java junit.swingui.TestRunner junit.samples.AllTests Swing测试范例的一个运行画面如下:
  • 10. 第二篇 JUnit安装设定
  • 11. 第二篇 JUnit安装设定如果您使用Eclipse整合开发环境(IDE),在Eclipse中内含JUnit套件,如果在使用它的话,请在Package Explorer中的顶层专案名称上按右键执行[properties],在[Java Build Path]的[libraries]标签页中按[Add JARs],选择junit.jar的所在位置将之加入,Eclipse是将之放在plugins\org.junit_3.8.1中:
  • 12. 第二篇 JUnit安装设定
  • 13. 第二篇 JUnit安装设定设定好之后在Package Explorer中就会出现JUnit套件相关资讯,接下来您就可以直接在Eclpise中新增TestCase文件。 其它的IDE可以至这个网页看看如何在其中使用JUnit: http://www.junit.org/news/ide/index.htm
  • 14. 第三篇 从Assert类开始Assert提供一个断言方法(assert methods)的集合,像是assertXXXX()等,这些方法都是静态方法,它们会在断言失败时丢出一个实作Throwable的异常,当中会带有断言失败的讯息。 我们先撰写一个简单的Student类别,以利用它来进行一些测试:
  • 15. 第三篇 从Assert类开始public class Student { private String number; private String name; private int score; public Student() { number = null; name = null; score = 0; } public Student (String number, String name, int score) { this.number = number; this.name = name; this.score = score; }
  • 16. 第三篇 从Assert类开始public String getNumber() { return number; } public String getName() { return name; } public int getScore() { return score; } public void setNumber(String number) { this.number = number; }
  • 17. 第三篇 从Assert类开始public void setName(String name) { this.name = name; } public void setScore(int score) { this.score = score; } public boolean equals(Object anObject) { if(anObject instanceof Student) { Student stu = (Student)anObject; return getName().equals(stu.getName()) && getNumber().equals(stu.getNumber()) && getScore() == stu.getScore(); }
  • 18. 第三篇 从Assert类开始return false; } } 我们先来看看在不使用JUnit的情况下,我们如何撰写一个测试表达式(Test Expression)以测试我们的类别撰写无误,例如您可能是这么撰写的: 代码:
  • 19. 第三篇 从Assert类开始public class Test2 {  public static void main(String[] args) { Student student = new Student("B83503124", "Justin", 100);                      if(!(student.getName() != null && student.getName().equals("Justin")))         System.out.println("getName method failure"); } } 同样一个功能,如果我们使用Assert提供的断言方法,则可以如下撰写:
  • 20. 第三篇 从Assert类开始import junit.framework.*; public class Test3 { public static void main(String[] args) { Student student = new Student("B83503124", "Justin", 100);                 Assert.assertEquals("getName metohd failure", "Justin", student.getName());     } }
  • 21. 第三篇 从Assert类开始Assert被归类于junit.framework中;如果断言失败,我们会收到一个Throwable物件,有这个例子中会是ComparisonFailure物件,例如我们故意让以上断言失败的话,可能收到以下的讯息: Exception in thread "main" junit.framework.ComparisonFailure: getName metohd failure expected: but was:         at junit.framework.Assert.assertEquals(Assert.java:81)   at Test3.main(Test3.java:7)
  • 22. 第三篇 从Assert类开始如果您不想收到这些冗长的讯息,您可以使用ComparisonFailure的getMessage()方法取得您指定的错误显示讯息,像是上面的"getName method failure"。 assertEquals()被重载为多个版本,以应付各种的资料型态,像是int、String等等,如果是比较物件的话,则根据equals()方法传回的true或false来比较,所以在您想要比较两个物件的field是否相同时,您要重新定义您的equals()方法,例如在我们的Student中,我们的equals()可以定义如下:
  • 23. 第三篇 从Assert类开始public boolean equals(Object anObject) { if(anObject instanceof Student) { Student stu = (Student)anObject; return getName().equals(stu.getName()) && getNumber().equals(stu.getNumber()) && getScore() == stu.getScore(); } return false; }
  • 24. 第三篇 从Assert类开始传统的测试表达式可以这么撰写: Student student1 = new Student("B83503124", "Justin", 100);  Student student2 = new Student("B83503124", "Justin", 100);            if(!student1.equals(student2))             System.out.println("instances  are unequal");
  • 25. 第三篇 从Assert类开始使用Assert的assertEquals()则可以这么撰写,代码如下: Student student1 = new Student("B83503124", "Justin", 100); Student student2 = new Student("B83503124", "Justin", 100);     Assert.assertEquals("instances  are unequal", student1, student2);
  • 26. 第三篇 从Assert类开始比较两个物件内容是否相同,通常是测试时很常进行的一个动作,而Assert类别也提供了像是assertTure()、assertFalse()、assertSame() 、assertNotSame()、assertNull()、assertNotNull()等等的方法,以便利各种测试场合的需要,例如我们上面的测试也可以这么撰写,代码如下: Student student1 = new Student("B83503124", "Justin", 100); Student student2 = new Student("B83503124", "Justin", 100); Assert.assertTure(student1.equals(student2))
  • 27. 第三篇 从Assert类开始在JUnit中会很常使用到Assert中的各种方法,但以上只是单一个Assert类别的几个方法介绍,还谈不上是一个测试,只要将各个测试进行组合,才谈的上是真正的测试开始,之后再慢慢一一介绍。
  • 28. 第四篇 测试案例——TestCase使用Assert类别中所提供的assertXXX()方法可以让您进行各种断言,如果断言失败,则可能传回AssertionFailedError或ComparisonFailure物件, 您可以利用try....catch区块收集并显示这些物件所夹带的讯息,然后重新返回测试。 然而事实上您不用自行设计,JUnit提供TestCase类别,您可以继承这个类别进行测试案例的撰写,并使用它的run()方法进行测试,TestCase物件会自行帮您收集测试失败时的相关讯息,之后您只要取得TestResult物件,就可以显示相关的讯息。 我们一步一步来看如何使用测试案例,首先还是那个简单的Student类别。
  • 29. 第四篇 测试案例——TestCase基本上您每设计一个类别,您就应该为它撰写一个测试,这个测试将成为您日后重构(refactor)的测试准测;我们先从简单的测试案例开始下手,如下所示: import junit.framework.*; public class TestIt extends TestCase { public TestIt(String name) {    super(name);    }
  • 30. 第四篇 测试案例——TestCasepublic void testGetMethod() {       Student student = new Student("B83503124", "Justin", 100);         assertEquals("B83503124", student.getNumber()); assertEquals("Justin", student.getName());       assertEquals(100, student.getScore());   }  public void testSetMethod() {      Student student = new Student();       student.setNumber("B83503124");        student.setName("Justin");       student.setScore(100);   assertEquals("B83503124", student.getNumber());       assertEquals("Justin", student.getName());      assertEquals(10, student.getScore());   } }
  • 31. 第四篇 测试案例——TestCase您如上继承TestCase类别,并撰写您想要进行的测试内容,TestCase继承了Assert类别,所以您可以直接使用Assert中的一些断言方法;我故意在testSetMethod()方法中撰写会发生错误的断言,这样待会才看得到一些错误报告,接下来我们撰写一个主函式来进行测试:
  • 32. 第四篇 测试案例——TestCaseimport java.util.*; import junit.framework.*; public class Main {   public static void main(String[] args) {         TestCase test1 = new TestIt("TestGet") {                    protected void runTest() {                             testGetMethod();                            }                   };      showResult(test1.run()); TestCase test2 = new TestIt("TestSet") {                     protected void runTest() {                             testSetMethod();                          }                      };      showResult(test2.run()); }
  • 33. 第四篇 测试案例——TestCasepublic static void showResult(TestResult result) {   if(result.errorCount() > 0) {            System.out.println("error: " + result.errorCount());          Enumeration error = result.errors();           while(error.hasMoreElements()) {        System.out.println(error.nextElement());            }        }       if(result.failureCount() > 0) {            System.out.println("failure: " + result.failureCount());         Enumeration failure = result.failures();            while(failure.hasMoreElements()) {               System.out.println(failure.nextElement());             }         }     } }
  • 34. 第四篇 测试案例——TestCase在这边我所使用的是测试案例进行测试时的一个静态指定方式,我使用一个匿名类别并重新定义runTest()方法,TestCase的run()方法会调用runTest()方法,并将测试的结果收集出来,传回一个TestResult物件。 TestResult物件拥有一些显示测试结果的相关方法,像是上面所示的errorCount()、failureCount()等等方法,errors()与failures()方法会传回Enumeration物件,测试的结果讯息就包括在当中,我们在这边直接将物件的内容显示出来,以显示一些简单的讯息,执行这个程式,其结果如下所示:
  • 35. 第四篇 测试案例——TestCasefailure: 1 TestSet(Main$2): expected:<10> but was:<100> 我们也可以使用动态的方法进行案例测试,这会使用到Java的Reflection机制来实作runTest()的内容,我们只要在建构TestCase物件时传入我们所想要的测试方法名称即可,例如:      TestCase test1 = new TestIt("testGetMethod");    showResult(test1.run());       TestCase test2 = new TestIt("testSetMethod"); showResult(test2.run());
  • 36. 第四篇 测试案例——TestCase将上面的程式片段取代之前main函式中的内容就可以进行测试,其结果显示与之前是相同的。 您可以自行执行TestCase的run()方法以收集测试结果并显示出来,事实上JUnit提供了TestRunner工具类别可以帮您省去这个工夫,而我们之后也会介绍使用TestSuite类别来进行测试的组织,这会比自行定义runTest()的实作并调用TestCase的run()方法好用的多。
  • 37. 第五篇 测试套件——TestSuite在之前的文件中,我们说明如何继承TestCase来撰写测试,如何静态或动态的调用我们撰写好的测试方法,以及如何使用收集到的TestResult将结果显示出来,您会问:有没有更好的方法可以更有弹性的组织测试方法、进行测试并显示结果?答案是有的,您可以将测试组织在一个测试套件(TestSuit)中,并使用TestRunner工具类别来帮您完成测试并显示结果。 首先还是先最初我们的那个Student类别。
  • 38. 第五篇 测试套件——TestSuite接下来我们撰写我们的一些测试方法 : import junit.framework.*; public class TestIt extends TestCase {     public TestIt(String name) {       super(name);     }     public void testGetMethod() {         Student student = new Student("B83503124", "Justin", 100);         assertEquals("B83503124", student.getNumber());         assertEquals("Justin", student.getName());         assertEquals(100, student.getScore());     }
  • 39. 第五篇 测试套件——TestSuitepublic void testSetMethod() {     Student student = new Student();       student.setNumber("B83503124");       student.setName("Justin");       student.setScore(100);       assertEquals("B83503124", student.getNumber());     assertEquals("Justin", student.getName());   assertEquals(10, student.getScore());    } }
  • 40. 第五篇 测试套件——TestSuite同样的,我们故意在testSetMethod()中造成测试失败,这样我们才可以看到一些测试讯息,我们先介绍最简单的一种测试执行方式,直接使用Reflection来取得要执行的测试: import junit.framework.*; public class Main {     public static void main(String[] args) {         junit.textui.TestRunner.run(TestIt.class);   } }
  • 41. 第五篇 测试套件——TestSuitejunit.textui.TestRunner是JUnit中所提供的一个测试工具类别,我们传给它一个TestIt的Class物件,这样它就可以使用Reflection机制,预设是将所有testXXX()开头的方法辨识出来并进行测试,然后自动收集结果并显示出来,执行结果如下: ..F Time: 0.027 There was 1 failure: 1) testSetMethod(TestIt)junit.framework.AssertionFailedError: expected:<10> butwas:<100>
  • 42. 第五篇 测试套件——TestSuite at TestIt.testSetMethod(TestIt.java:22)   at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)       at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)         at Main.main(Main.java:5) FAILURES!!! Tests run: 2,  Failures: 1,  Errors: 0
  • 43. 第五篇 测试套件——TestSuite这个测试结果显示比自行处理讯息显示来得丰富的多了。 JUnit也提供了图形介面的TestRunner类别,您可以用以下的程式来取代main()中的陈述: AWT测试介面: 代码:junit.awtui.TestRunner.run(TestIt.class); Swing测试介面: 代码:junit.swingui.TestRunner.run(TestIt.class);
  • 44. 第五篇 测试套件——TestSuite下图为Swing测试界面的一个画面:
  • 45. 第五篇 测试套件——TestSuite您可以在TestIt重新定义suite()方法,在JUnit 2.0之后,提供一个简单的动态方法产生TestSuit物件,您只要将TestCase的Class物件传给它作为参数就可以了,如下所示: public static Test suite() {     return new TestSuite(TestIt.class); }
  • 46. 第五篇 测试套件——TestSuite这个方式会使用Reflection来辨识出所有的testX()方法并加入测试,注意到传回型态是Test,因为JUnit的TestCase与TestSuite是组合(Composite)模式关系,TestSuite实现了Test接口,关于组合模式的说明请见: http://www.caterpillar.onlyfun.net/phpBB2/viewtopic.php?t=768 您可以使用静态的方法来建构TestSuite物件,例如:
  • 47. 第五篇 测试套件——TestSuitepublic static Test suite() { TestSuite suite= new TestSuite();     suite.addTest( new TestIt("TestGet") {         protected void runTest() { testGetMethod(); }         }     );     suite.addTest(new TestIt("TestSet") {         protected void runTest() { testSetMethod(); }       }   );    return suite; }
  • 48. 第五篇 测试套件——TestSuite如果您瞭解之前的TestCase文件介绍,您应该也可以知道使用动态方法建TestSuite: public static Test suite() {     TestSuite suite= new TestSuite();     suite.addTest(new TestIt("testGetMethod"));  suite.addTest(new TestIt("testSetMethod"));    return suite; } 定义好suite()方法之后,您可以如下撰写测试:
  • 49. 第五篇 测试套件——TestSuiteimport junit.framework.*;   public class Main {     public static void main(String[] args) {   junit.textui.TestRunner.run(TestIt.suite());    } } 如果您使用swingui的测试程式,您可以直接这么执行所定义的测试套件: junit.swingui.TestRunner.run(TestIt.class); 使用swingui的测试程式时,如果不定义suite()方法,则预设会取出所有的testXXXX()进行测试。
  • 50. 第五篇 测试套件——TestSuite您也许会问到,如果不使用TestRunner,可不可以自行定义测试的执行,答案是可以的,您甚至可以合併其它的测试套件: public static Test suite() { TestSuite suite= new TestSuite();    suite.addTest(new TestIt("testGetMethod"));   suite.addTest(new TestIt("testSetMethod"));    suite.addTestSuite(AnotherTestSuiteImplementation.class); return suite; }
  • 51. 第五篇 测试套件——TestSuitepublic class Main {     public static void main(String[] args) {           TestSuite suite = TestIt.suite();           TestResult result = new TestResult();       suite.run(result);       showResult(result); } public static void showResult(TestResult result) {        if(result.errorCount() > 0) {    System.out.println("error: " + result.errorCount());     Enumeration error = result.errors();         while(error.hasMoreElements()) {   System.out.println(error.nextElement());       }  }  
  • 52. 第五篇 测试套件——TestSuiteif(result.failureCount() > 0) {             System.out.println("failure: " + result.failureCount());       Enumeration failure = result.failures();         while(failure.hasMoreElements()) {         System.out.println(failure.nextElement());         }   } } } JUnit提供各种弹性的测试组合与方便的工具类别,以上所示的只是几个简单的例子,您可以根据自行的需求来进行测试的组织。
  • 53. 第六篇 JUnit——基本建构过程了解JUnit的基本建构过程,可以了解测试的目标是什么、测试的基本流程与架构,而进一步的了解JUnit是如何运作的。 这些文章主要从下面这个网址的文章撷取重点部份,并以自己的心得撰写而成,如果您需要更多详细的内容或是原音重现,可以直接参考原文章: http://junit.sourceforge.net/doc/cookstour/cookstour.htm
  • 54. 第六篇 JUnit——基本建构过程我们将JUnit的构造用UML类别图来表示,您可以更清楚的看出JUnit的基本架构:
  • 55. 第七篇 JUnit——组合测试我们可以在继承TestCase之后,撰写一个静态suite()方法组合单元测试,也就是使用addTest()方法,例如: public static Test suite() {     TestSuite suite= new TestSuite();     suite.addTest(new TestIt("testGetMethod")); suite.addTest(new TestIt("testSetMethod"));   suite.addTestSuite(AnotherTestSuiteImplementation.class); return suite; }
  • 56. 第七篇 JUnit——组合测试TestCase与TestSuite都继承了接口,执行测试时会调用它们的run()方法,这是组合(composite)模式的应用,这意味著我们可以组合TestCase或TestSuite为一个更大的测试套件。 如果您想要将之前所曾经撰写过的TestCase集合一起来一起进行测试,您可以撰写一个TestAll之类的类别,基本上它只简单的含有一个静态方法suite(),它传回一个实作Test的物件,也就是TestSuite,例如:
  • 57. 第七篇 JUnit——组合测试import junit.framework.Test; import junit.framework.TestSuite; public class TestAll {     public static Test suite() {           TestSuite suite = new TestSuite("TestAll class");         suite.addTestSuite(TestCase1.class);        suite.addTestSuite(TestCase2.class);         suite.addTest(TestCase3.suite()); return suite;    } }
  • 58. 第七篇 JUnit——组合测试这个例子中示范了两种方式,一种是直接使用Reflection机制识别testXXX()方法出来,一种是直接使用TestCase中定义的静态suite()方法,接下来您就可以使用TestRunner来执行所有的测试: junit.swingui.TestRunner.run(TestAll.suite());
  • 59. 第八篇 与EclEmma一起工作EclEmma 是一个能帮助开发人员考察测试覆盖率的 Eclipse 开源插件。 获得 EclEmma的支持 从站点下载 EclEmma 的zip 文件并解压到 eclipse 所在的目录中; 安装完成并重新启动 Eclipse 之后,工具栏上应该出现一个新的按钮: EclEmma插件安装成功,现在可以让JUnit与EclEmma一起工作。
  • 60. 第八篇 与EclEmma一起工作对应用程序进行测试并统计覆盖率 以测试用例SingleTest为例说明如何测试覆盖率: 第一步:选择文件并按如下顺序操作 SingleTest.java→点击右键→CoverageAs→ Open Coverage Dialog,打开的界面如下图:
  • 61. 第八篇 与EclEmma一起工作
  • 62. 第八篇 与EclEmma一起工作最后一步: 点击Coverage按钮, EclEmma就会开始运行, EclEmma 用不同的色彩标示了源代码的测试情况。 其中,绿色的行表示该行代码被完整的执行,红 色部分表示该行代码根本没有被执行 。 执行完毕之后, TestCalculator.java的窗口如下图所示:
  • 63. 第八篇 与EclEmma一起工作
  • 64. 第八篇 与EclEmma一起工作EclEmma Report 的导出 右键点击选择Export Report弹出窗口如下:
  • 65. 第八篇 与EclEmma一起工作选择导出文件类型html,xml,text,emma session file四种类型选取其一导出成功. EclEmma Report 的导入 相反的过程,右键点击Import Session选择要导入的文件导入成功. 通过导入和到处操作,我们可以看到测试的覆盖率,便于比较和改善,对比测试用例的优劣.
  • 66. 谢谢!