JUnit4使用教程

zhangleijie 贡献于2011-05-07

作者 SunnyLUO  创建于2007-12-01 03:22:00   修改者SunnyLUO  修改于2008-10-29 02:21:00字数11019

文档摘要:本文主要通过一个实例介绍了如何使用JUnit4提供的各种功能开展有效的单元测试。 JUnit是Java社区中知名度最高的单元测试工具。它诞生于 1997 年,由 Erich Gamma 和 Kent Beck 共同开发完成。其中 Erich Gamma 是经典著作《设计模式:可复用面向对象软件的基础》一书的作者之一,并在 Eclipse 中有很大的贡献;Kent Beck 则是一位极限编程(XP)方面的专家和先驱。
关键词:

JUnit4使用教程 本文主要通过一个实例介绍了如何使用 JUnit 4 提供的各种功能开展有效的单元测试。 JUnit 是 Java 社区中知名度最高的单元测试工具。它诞生于 1997 年,由 Erich Gamma 和 Kent Beck 共同开发完成。其中 Erich Gamma 是经典著作《设计模式:可复用面向对象软件的基础》一书的作者之一,并在 Eclipse 中有很大的贡献;Kent Beck 则是一位极限编程(XP)方面的专家和先驱。 麻雀虽小,五脏俱全。JUnit 设计的非常小巧,但是功能却非常强大。Martin Fowler 如此评价 JUnit:在软件开发领域,从来就没有如此少的代码起到了如此重要的作用。它大大简化了开发人员执行单元测试的难度,特别是 JUnit 4 使用 Java 5 中的注解(annotation)使测试变得更加简单。 在单元测试前首先规划单元测试代码应放在什么地方。把它和被测试代码混在一起,这显然会照成混乱,因为单元测试代码是不会出现在最终产品中的。建议分别为单元测试代码与被测试代码创建单独的目录,并保证测试代码和被测试代码使用相同的包名。这样既保证了代码的分离,同时还保证了查找的方便。 下面的例子来自开发实践:工具类 WordDealUtil 中的静态方法 wordFormat4DB 是专用于处理 Java 对象名称向数据库表名转换的方法(可以在代码注释中可以得到更多详细的内容)。下面是第一次编码完成后大致情形: package cooljunit; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 对名称、地址等字符串格式的内容进行格式检查或者格式化的工具类 */ public class WordDealUtil { /** * 将Java对象名称(每个单词的头字母大写)按照数据库命名的习惯进行格式化 * 格式化后的数据为小写字母,并且使用下划线分割命名单词 * 例如:employeeInfo 经过格式化之后变为 employee_info * @param name Java对象名称 */ public static String wordFormat4DB(String name){ Pattern p = Pattern.compile("[A-Z]"); Matcher m = p.matcher(name); StringBuffer sb = new StringBuffer(); while(m.find()){ m.appendReplacement(sb, "_"+m.group()); } return m.appendTail(sb).toString().toLowerCase(); } } 它是否能按照预期的效果执行呢?尝试为它编写 JUnit 单元测试代码如下: package cooljunit; import static org.junit.Assert.assertEquals; import org.junit.Test; import org.junit.runner.JUnitCore; public class TestWordDealUtil { //测试wordFormat4DB正常运行的情况 @Test public void wordFormat4DBNormal(){ String target = "employeeInfo"; String result = WordDealUtil.wordFormat4DB(target); assertEquals("employee_info", result); // System.out.println(“employee_info”.equals(result)); } public static void main(String[] args) { JUnitCore().main(new String[] { “TestWordDealUtil” }); } } 测试类 TestWordDealUtil 之所以使用“Test”开头,完全是为了更好的区分测试类与被测试类。测试方法 wordFormat4DBNormal 调用执行被测试方法 WordDealUtil.wordFormat4DB,以判断运行结果是否达到设计预期的效果。需要注意的是,测试方法 wordFormat4DBNormal 需要按照一定的规范书写: 1. 测试方法必须使用注解 org.junit.Test 修饰。 2. 测试方法必须使用public void修饰,而且不能带有任何参数。 测试方法中要处理的字符串为“employeeInfo”,按照设计目的,处理后的结果应该为“employee_info”。assertEquals 是由 JUnit 提供的一系列判断测试结果是否正确的静态断言方法(位于类 org.junit.Assert 中)之一,使用它将执行结果 result 和预期值“employee_info”进行比较,来判断测试是否成功。 下面简单介绍一下静态类org.junit.Assert。 该类主要包含以下22个方法: 1.assertEquals(),8个重载,用来查看对象中存的值是否是期待的值,与字符串比较中使用的equals()方法类似; 2.assertFalse()和assertTrue(),各2个重载,用来查看变量是是否为false或true,如果assertFalse()查看的变量的值是false则测试成功,如果是true则失败,assertTrue()与之相反; 3.assertSame()和assertNotSame(),各2个重载,用来比较两个对象的引用是否相等和不相等,类似于通过“==”和“!=”比较两个对象; 4.assertNull()和assertNotNull(),各2个重载,用来查看对象是否为空和不为空; 5.fail (),2个重载,意为失败,用来抛出AssertionError错误。有两个用途:首先是在测试驱动开发中,由于测试用例都是在被测试的类之前编写,而写成时又不清楚其正确与否,此时就可以使用fail方法抛出错误进行模拟;其次是抛出意外的错误,比如要测试的内容是从数据库中读取的数据是否正确,而导致错误的原因却是数据库连接失败。 单元测试的范围要全面,比如对边界值、正常值、错误值得测试;对代码可能出现的问题要全面预测,而这也正是需求分析、详细设计环节中要考虑的。显然,以上测试才刚刚开始,需继续补充一些对特殊情况的测试: public class TestWordDealUtil { …… //测试 null 时的处理情况 @Test public void wordFormat4DBNull(){ String target = null; String result = WordDealUtil.wordFormat4DB(target); assertNull(result); } //测试空字符串的处理情况 @Test public void wordFormat4DBEmpty(){ String target = ""; String result = WordDealUtil.wordFormat4DB(target); assertEquals("", result); } //测试当首字母大写时的情况 @Test public void wordFormat4DBegin(){ String target = "EmployeeInfo"; String result = WordDealUtil.wordFormat4DB(target); assertEquals("employee_info", result); } //测试当尾字母为大写时的情况 @Test public void wordFormat4DBEnd(){ String target = "employeeInfoA"; String result = WordDealUtil.wordFormat4DB(target); assertEquals("employee_info_a", result); } //测试多个相连字母大写时的情况 @Test public void wordFormat4DBTogether(){ String target = "employeeAInfo"; String result = WordDealUtil.wordFormat4DB(target); assertEquals("employee_a_info", result); } …… } 再次运行测试。此时,JUnit提示有两个测试情况未通过测试——当首字母大写时得到的处理结果与预期的有偏差,造成测试失败(failure);而当测试对 null 的处理结果时,则直接抛出了异常——测试错误(error)。显然,被测试代码中并没有对首字母大写和 null 这两种特殊情况进行处理,修改如下: //修改后的方法wordFormat4DB /** * 将Java对象名称(每个单词的头字母大写)按照数据库命名的习惯进行格式化 * 格式化后的数据为小写字母,并且使用下划线分割命名单词 * 如果参数name为null,则返回null * 例如:employeeInfo 经过格式化之后变为 employee_info * @param name Java对象名称 */ public static String wordFormat4DB(String name){ if(name == null){ return null; } Pattern p = Pattern.compile("[A-Z]"); Matcher m = p.matcher(name); StringBuffer sb = new StringBuffer(); while(m.find()){ if(m.start() != 0) m.appendReplacement(sb, ("_"+m.group()).toLowerCase()); } return m.appendTail(sb).toString().toLowerCase(); } JUnit 将测试失败的情况分为两种:failure 和 error。Failure 一般由单元测试使用的断言方法判断失败引起,它表示在测试点发现了问题;而 error 则是由代码异常引起,这是测试目的之外的发现,它可能产生于测试代码本身的错误(测试代码也是代码,同样无法保证完全没有缺陷),也可能是被测试代码中的一个隐藏的bug。 再次运行测试。通过对 WordDealUtil.wordFormat4DB 比较全面的单元测试,现在的代码已经比较稳定,可以作为 API 的一部分提供给其它模块使用了。 当然,JUnit 提供的功能决不仅仅如此简单,在接下来的内容中,会看到 JUnit 中很多有用的特性,掌握它们对灵活的编写单元测试代码非常有帮助。 Fixture 何谓 Fixture?它是指在执行一个或者多个测试方法时需要的一系列公共资源或者数据,例如测试环境,测试数据等等。JUnit 专门提供了设置公共 Fixture 的方法,同一测试类中的所有测试方法都可以共用它来初始化 Fixture 和注销 Fixture。和编写 JUnit 测试方法一样,公共 Fixture 的设置也很简单,只需要: 1. 使用注解 org.junit.Before修饰用于初始化 Fixture 的方法。 2. 使用注解 org.junit.After修饰用于注销 Fixture 的方法。 3. 保证这两种方法都使用public void修饰,而且不能带有任何参数。 遵循上面的三条原则,编写出的代码大体是这个样子: //初始化Fixture方法 @Before public void init(){……} //注销Fixture方法 @After public void destroy(){……} 这样,在每一个测试方法执行之前,JUnit 会保证 init 方法已经提前初始化测试环境,而当此测试方法执行完毕之后,JUnit 又会调用 destroy 方法注销测试环境。注意是每一个测试方法的执行都会触发对公共 Fixture 的设置,也就是说使用注解 Before 或者 After 修饰的公共 Fixture 设置方法是方法级别的(图1)。这样便可以保证各个独立的测试之间互不干扰,以免其它测试代码修改测试环境或者测试数据影响到其它测试代码的准确性。 图1 方法级别 Fixture 执行示意图 可是,这种 Fixture 设置方式还是引来了批评,因为它效率低下,特别是在设置 Fixture 非常耗时的情况下(例如设置数据库链接)。而且对于不会发生变化的测试环境或者测试数据来说,是不会影响到测试方法的执行结果的,也就没有必要针对每一个测试方法重新设置一次 Fixture。因此在 JUnit 4 中引入了类级别的 Fixture 设置方法,编写规范如下: 1. 使用注解org.junit.BeforeClass修饰用于初始化Fixture 的方法。 2. 使用注解 org.junit.AfterClass 修饰用于注销 Fixture 的方法。 3. 保证这两种方法都使用 public static void 修饰,而且不能带有任何参数。 类级别的 Fixture 仅会在测试类中所有测试方法执行之前执行初始化,并在全部测试方法测试完毕之后执行注销方法(图6)。代码范本如下: //类级别Fixture初始化方法 @BeforeClass public static void dbInit(){……} //类级别Fixture注销方法 @AfterClass public static void dbClose(){……} 图2 类级别 Fixture 执行示意图 运行以下例子,可以更深刻理解方法的执行顺序: import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.JUnitCore; /** * 1:标注BeforeClass的方法最先被执行,且只执行一次 * 2:标注AfterClass的方法最后被执行,且只执行一次 * 3:所有标注After的方法,会在每一个Test方法执行完后被执行一遍 * Before则是在每一个Test方法执行之前被执行一遍 * 4:After和Before执行的顺序与这些被标注的方法在代码中的位置无关,只与Junit内部排序规则相关. * 5:Ignore标注的方法将会被加入测试方法序列,但是不被执行. */ public class Example { @BeforeClass public static void init(){ System.out.println("public static void init ..."); } @BeforeClass public static void init1(){ System.out.println("public static void init1 ..."); } @AfterClass public static void release(){ System.out.println("public static release ..."); } @Before public void before(){ System.out.println("before ..."); } @Before public void before1(){ System.out.println("before1 ..."); } @Test public void example() { System.out.println("---------Test Begin--------------"); System.out.println("Hello..."); } @After public void close1() { System.out.println("After,should close 1..."); } @Test public void example1() { System.out.println("---------Test Begin--------------"); System.out.println("Hello 1..."); } @After public void close() { System.out.println("After,should close..."); } @Test public void example2() { System.out.println("---------Test Begin--------------"); System.out.println("Hello 2..."); } @After public void close2() { System.out.println("After,should close 2..."); } @Ignore("Not retry yet") public void testIndexOut() { System.out.println("Exception test..."); } public Example() { System.out.println("Example construct running..."); } public static void main(String[] args) { JUnitCore().main(new String[] { “Example” }); } } 异常以及时间测试 注解org.junit.Test 中有两个非常有用的参数:expected和timeout。参数 expected代表测试方法期望抛出指定的异常,如果运行测试并没有抛出这个异常,则JUnit 会认为这个测试没有通过。这为验证被测试方法在错误的情况下是否会抛出预定的异常提供了便利。举例来说,方法supportDBChecker用于检查用户使用的数据库版本是否在系统的支持的范围之内,如果用户使用了不被支持的数据库版本,则会抛出运行时异常UnsupportedDBVersionException。测试方法 supportDBChecker在数据库版本不支持时是否会抛出指定异常的单元测试方法大体如下: @Test(expected=UnsupportedDBVersionException.class) public void unsupportedDBCheck(){ …… } 注解org.junit.Test的另一个参数timeout,指定被测试方法被允许运行的最长时间应该是多少,如果测试方法运行时间超过了指定的毫秒数,则JUnit认为测试失败。这个参数对于性能测试有一定的帮助。例如,如果解析一份自定义的XML文档花费了多于1秒的时间,就需要重新考虑XML结构的设计,那单元测试方法可以这样来写: @Test(timeout=1000) public void selfXMLReader(){ …… } 忽略测试方法 JUnit提供注解org.junit.Ignore用于暂时忽略某个测试方法,因为有时候由于测试环境受限,并不能保证每一个测试方法都能正确运行。例如下面的代码便表示由于没有了数据库链接,提示JUnit忽略测试方法 unsupportedDBCheck: @ Ignore(“db is down”) @Test(expected=UnsupportedDBVersionException.class) public void unsupportedDBCheck(){ …… } 但是一定要小心。注解org.junit.Ignore只能用于暂时的忽略测试,如果需要永远忽略这些测试,一定要确认被测试代码不再需要这些测试方法,以免忽略必要的测试点。 测试运行器 测试运行器是又一个新概念,JUnit中所有的测试方法都是由它负责执行的。JUnit为单元测试提供了默认的测试运行器,但JUnit 并没有限制必须使用默认的运行器。相反,不仅可以定制自己的运行器(所有的运行器都继承自org.junit.runner.Runner),而且还可以为每一个测试类指定使用某个具体的运行器。指定方法也很简单,使用注解org.junit.runner.RunWith在测试类上显式的声明要使用的运行器即可: @RunWith(CustomTestRunner.class) public class TestWordDealUtil { …… } 显而易见,如果测试类没有显式的声明使用哪一个测试运行器,JUnit会启动默认的测试运行器执行测试类(比如上面提及的单元测试代码)。一般情况下,默认测试运行器可以应对绝大多数的单元测试要求;当使用JUnit提供的一些高级特性(例如即将介绍的两个特性)或者针对特殊需求定制JUnit测试方式时,显式的声明测试运行器就必不可少了。 测试套件 在实际项目中,随着项目进度的开展,单元测试类会越来越多,可是直到现在还只会一个一个的单独运行测试类,这在实际项目实践中肯定是不可行的。为了解决这个问题,JUnit 提供了一种批量运行测试类的方法,叫做测试套件。这样,每次需要验证系统功能正确性时,只执行一个或几个测试套件便可以了。测试套件的写法非常简单,只需要遵循以下规则: 1. 创建一个空类作为测试套件的入口。 2. 使用注解org.junit.runner.RunWith和org.junit.runners.Suite. SuiteClasses修饰这个空类。 3. 将org.junit.runners.Suite作为参数传入注解RunWith,以提示 JUnit为此类使用套件运行器执行。 4. 将需要放入此测试套件的测试类组成数组作为注解 SuiteClasses的参数。 5. 保证这个空类使用public修饰,而且存在公开的不带有任何参数的构造函数。 package cooljunit; import org.junit.runner.RunWith; import org.junit.runners.Suite; import org.junit.runner.JUnitCore; …… /** * 批量测试包中的测试类 */ @RunWith(Suite.class) @Suite.SuiteClasses({TestWordDealUtil.class,...}) public class RunAllUtilTestsSuite { public static void main(String[] args) { JUnitCore().main(new String[] { “RunAllUtilTestsSuite” }); } } 上例代码中,将前文提到的测试类 TestWordDealUtil 放入了测试套件 RunAllUtilTestsSuite 中,运行测试套件,可以看到测试类 TestWordDealUtil被调用执行了。测试套件中不仅可以包含基本的测试类,而且可以包含其它的测试套件,这样可以很方便的分层管理不同模块的单元测试代码。但是,一定要保证测试套件之间没有循环包含关系,否则无尽的循环就会出现 在面前……。 参数化测试 为了保证单元测试的严谨性,通常需要模拟不同情况来测试方法的处理能力,为此要编写大量的单元测试方法。可是这些测试方法都是大同小异:代码结构都是相同的,不同的仅仅是测试数据和期望值。有没有更好的方法将测试方法中相同的代码结构提取出来,提高代码的重用度,减少复制粘贴代码的烦恼?在以前的 JUnit 版本上,并没有好的解决方法,而现在可以使用 JUnit 提供的参数化测试方式应对这个问题。只需要遵循以下规则: 1. 为准备使用参数化测试的测试类指定特殊的运行器 org.junit.runners.Parameterized。 2. 为测试类声明几个变量,分别用于存放期望值和测试所用数据。 3. 为测试类声明一个使用注解org.junit.runners.Parameterized. Parameters 修饰的,返回值为java.util.Collection的公共静态方法,并在此方法中初始化所有需要测试的参数对。 4. 为测试类声明一个带有参数的公共构造函数,并在其中为第二个环节中声明的几个变量赋值。 5. 编写测试方法,使用定义的变量作为参数进行测试。 按照这个标准,重新改造一番单元测试代码: package cooljunit; import static org.junit.Assert.assertEquals; import java.util.Arrays; import java.util.Collection; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import org.junit.runner.JUnitCore; @RunWith(Parameterized.class) public class TestWordDealUtilWithParam { private String expected; private String target; @Parameters public static Collection words(){ return Arrays.asList(new Object[][]{ {"employee_info", "employeeInfo"}, //测试一般的处理情况 {null, null}, //测试 null 时的处理情况 {"", ""}, //测试空字符串时的处理情况 {"employee_info", "EmployeeInfo"}, //测试当首字母大写时的情况 {"employee_info_a", "employeeInfoA"}, //测试当尾字母为大写时的情况 {"employee_a_info", "employeeAInfo"}//测试多个相连字母大写时的情况 }); } /** * 构造函数 * @param expected 期望的测试结果,对应参数集中的第一个参数 * @param target 测试数据,对应参数集中的第二个参数 */ public TestWordDealUtilWithParam(String expected , String target){ this.expected = expected; this.target = target; } /** * 测试将 Java 对象名称到数据库名称的转换 */ @Test public void wordFormat4DB(){ assertEquals(expected, WordDealUtil.wordFormat4DB(target)); } public static void main(String[] args) { JUnitCore().main(new String[] { “TestWordDealUtilWithParam” }); } } 在静态方法 words 中,使用二维数组来构建测试所需要的参数列表,其中每个数组中的元素的放置顺序并没有什么要求,只要和构造函数中的顺序保持一致就可以了。现在如果再增加一种测试情况,只需要在静态方法 words 中添加相应的数组即可,不再需要复制粘贴出一个新的方法出来了。

下载文档到电脑,查找使用更方便

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 15 金币 [ 分享文档获得金币 ] 0 人已下载

下载文档