内容


深入探索 JUnit 4

使用 Java 5 注释精简测试

Comments

开始之前

关于本教程

引入 Java 5 注释为 JUnit 带来了显著改变,使它从一个受广大开发人员了解和喜爱的测试框架转变成了一个更为精简但却不那么为人熟知的框架。在本教程中,我将探讨 JUnit 4 最重要的转变,并介绍一些您也许已经耳闻但还没用过的激动人心的新功能。

目标

本教程将循序渐进地向您讲述 JUnit 4 的基本概念,侧重于新的 Java 5 注释。通过这个一小时教程的学习,您将能够理解 JUnit 4 的主要改变,也将熟悉这些功能,如异常测试、参数测试以及新的灵活固件模型。您还将了解如何声明测试,如何使用注释(而不是套件)在运行测试前从逻辑上对其分组,如何在 Eclipse 3.2 或 Ant 中运行测试,以及如何从命令行运行测试。

先决条件

为更好地学习本教程,您应该大体熟悉 Java 开发。本教程也假设您理解开发人员测试的价值,并熟悉基本模式匹配。为学习运行 JUnit 4 测试这个章节,您应该能够将 Eclipse 3.2 作为一个 IDE 使用,也应该能够使用 Ant 1.6 或更新版本。本教程不要求您熟悉 JUnit 以前的版本。

系统需求

为学习本教程及试验本教程中的代码,需要一份 Sun 的 JDK 1.5.0_09(或更新版本)的工作安装版,或针对 Java 技术 1.5.0 SR3 的 IBM 开发工具包的工作安装版。对于在 Eclipse 中运行 JUnit 4 这些章节,需要一份 Eclipse 3.2 或更新版本的工作安装版。对于有关 Ant 的章节,需要 1.6 版或更新版。

本教程推荐的系统配置如下:

  • 系统要支持 Sun JDK 1.5.0_09 (或更新版本)或针对 Java 技术 1.5.0 SR3 的 IBM 开发工具包,至少有 500 MB 主存。
  • 至少有 20 MB 磁盘空间来安装软件组件和文中提到的样例。

本教程的说明基于 Microsoft Windows 操作系统。教程中涵盖的所有工具也可以在 Linux 和 UNIX 系统中运行。

JUnit 4 的新功能

借助 Java 5 注释,JUnit 4 比从前更轻(量级),也更加灵活。JUnit 4 放弃了严格的命名规范和继承层次,转向了一些令人激动的新功能。下面是一份关于 JUnit 4 新功能的快速列表:

  • 参数测试
  • 异常测试
  • 超时测试
  • 灵活固件
  • 忽略测试的简单方法
  • 对测试进行逻辑分组的新方法

首先,我要解释 JUnit 4 最重要最令人激动的改变,为在稍后的章节中介绍这些功能和更多新功能做好准备。

摒弃旧规则

在将 Java 5 注释添加到 JUnit 4 之前,该框架已经建立起两条对其运行能力至为重要的规则。第一条规则是:JUnit 明确要求任何作为逻辑测试而编写的方法要以 test 这个词开头。任何以该词开头的方法,如 testUserCreate,均应按照一个定义良好的测试过程来执行,从而保证固件在测试方法前和测试方法后均要执行。第二条规则:为了让 JUnit 识别包含测试的类对象,要求类本身从 JUnit 的 TestCase (或它的一些派生类)中扩展。破坏了这两条规则中任意一条规则的测试将不会运行

清单 1 是一个在 JUnit 4 之前编写的 JUnit 测试

清单 1. 有必要这么难吗?
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import junit.framework.TestCase;

public class RegularExpressionTest extends TestCase {
	
 private String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
 private Pattern pattern;

 protected void setUp() throws Exception {
  this.pattern = Pattern.compile(this.zipRegEx);
 }

 public void testZipCode() throws Exception{		 
  Matcher mtcher = this.pattern.matcher("22101");
  boolean isValid = mtcher.matches();		
  assertTrue("Pattern did not validate zip code", isValid);
 }
}

引入新方法

JUnit 4 使用 Java 5 注释来彻底淘汰了这两条规则。现在,不再需要类层次,而且那些想要实现测试功能的方法只需要用一个新定义的 @Test 注释来修饰就可以了。

清单 2 显示了与 清单 1 相同的测试,只不过这次用注释进行了重新定义:

清单 2. 含注释的测试
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertTrue;

public class RegularExpressionTest {
 private static String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
 private static Pattern pattern;

 @BeforeClass
 public static void setUpBeforeClass() throws Exception {
  pattern = Pattern.compile(zipRegEx);
 }

 @Test
 public void verifyGoodZipCode() throws Exception{		 
  Matcher mtcher = this.pattern.matcher("22101");
  boolean isValid = mtcher.matches();		
  assertTrue("Pattern did not validate zip code", isValid);
 }
}

清单 2 中的测试在编码上也许并不会比原来简单很多,但却一定更加容易理解。

简化文档

注释的一个有用的副作用是它们将方法要做的事明确地文档化,而 需要对该框架的内部模型有深入的理解。还有什么比用 @Test 修饰测试方法更简洁明了的呢?这是对旧版 JUnit 的巨大改进,旧版 JUnit 要求您对 JUnit 规范要相当熟悉,即使您想要的仅仅是理解每个方法对一个完整测试用例的贡献。

在解析已经写好的测试方面,注释能提供很多帮助,但当您看到注释为编写测试的过程带来的额外帮助后,就会更被它们所吸引。

用注释进行测试

Java 5 注释让 JUnit 4 成为了一个与以往版本显著不同的框架。在本节中,您将了解如何在一些关键的地方(如测试声明、异常测试以及超时测试)使用注释,以及如何忽略不想要或无用的测试。

测试声明

在 JUnit 4 中声明一个测试实际上就是用 @Test 注释修饰测试方法。注意,不需要从任何特定的类中扩展,如清单 3 所示:

清单 3. JUnit 4 中的测试声明
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertFalse;

public class RegularExpressionTest {
 private static String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
 private static Pattern pattern;
  
 @BeforeClass
 public static void setUpBeforeClass() throws Exception {
  pattern = Pattern.compile(zipRegEx);
 }

 @Test
 public void verifyZipCodeNoMatch() throws Exception{		 
  Matcher mtcher = this.pattern.matcher("2211");
  boolean notValid = mtcher.matches();		
  assertFalse("Pattern did validate zip code", notValid);
 }
}

有关静态导入的一点说明

我使用了 Java 5 的静态导入功能来导入清单 3 中 Assert 类的 assertFalse() 方法。这是因为不同于以往的 JUnit 版本,测试类不从 TestCase 中扩展。

异常测试

在以往的 JUnit 版本中,指定测试抛出 Exception 通常都是一个很好的做法。只有在测试一个特别的异常时,才会想要忽略这条规则。如果测试抛出一个异常,该框架会报告一次失败。

如果真的想要测试一个特别的异常,JUnit 4 的 @Test 注释支持一个 expected 参数,该参数意在表示测试在执行中抛出的异常类型。

下面以一个简单的比较来说明新参数的不同之处。

JUnit 3.8 中的异常测试

清单 4 中的 JUnit 3.8 测试(命名为 testZipCodeGroupException())验证了试图获取第三组正则表达式(我声明的)将会导致一个 IndexOutOfBoundsException

清单 4. 在 JUnit 3.8 中测试一个异常
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import junit.framework.TestCase;

public class RegularExpressionTest extends TestCase {

 private String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
 private Pattern pattern;

 protected void setUp() throws Exception {
  this.pattern = Pattern.compile(this.zipRegEx);
 }

 public void testZipCodeGroupException() throws Exception{		 
  Matcher mtcher = this.pattern.matcher("22101-5051");
  boolean isValid = mtcher.matches();			
  try{
   mtcher.group(2);
   fail("No exception was thrown");
  }catch(IndexOutOfBoundsException e){
  }
 }
}

这个旧版的 JUnit 需要我为这么一个简单的测试写那么多代码 —— 即编写一个 try/catch,如果没有捕捉到异常,就会让测试失败。

JUnit 4 中的异常测试

除了使用新的 expected 参数外,清单 5 中的异常测试和清单 4 中的没多大区别。(注意,我可以通过将 IndexOutOfBoundsException 异常传入到 @Test 注释来翻新清单 4 中的测试。)

清单 5. 含 ‘expected’ 参数的异常测试
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.BeforeClass;
import org.junit.Test;

public class RegularExpressionJUnit4Test {
 private static String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
 private static Pattern pattern;

 @BeforeClass
 public static void setUpBeforeClass() throws Exception {
  pattern = Pattern.compile(zipRegEx);
 }

 @Test(expected=IndexOutOfBoundsException.class)
 public void verifyZipCodeGroupException() throws Exception{		
  Matcher mtcher = this.pattern.matcher("22101-5051");
  boolean isValid = mtcher.matches();			
  mtcher.group(2);		
 }
}

超时测试

在 JUnit 4 中,测试用例可以将超时值作为参数。正如在清单 6 中所见,timeout 值代表测试能够运行的最长时间:如果时间超过,测试就会失败。

清单 6. 含超时值的测试
@Test(timeout=1)
public void verifyFastZipCodeMatch() throws Exception{		
 Pattern pattern = Pattern.compile("^\\d{5}([\\-]\\d{4})?$"); 
 Matcher mtcher = pattern.matcher("22011");
 boolean isValid = mtcher.matches();		
 assertTrue("Pattern did not validate zip code", isValid);
}

含超时的测试很容易:用跟着 timeout 值的 @Test 修饰一个方法,就能获得一个自动的超时测试!

忽略测试

在 JUnit 4 以前,忽略坏掉的或不完整的测试让人很头疼。如果想让框架忽略一个特别的测试,不得不修改测试名,故意不让它遵循测试的命名规则。例如,我经常把一个 “_” 放到测试名前面,来提示该测试不在当前运行。

JUnit 4 引入了一个被适当命名为 @Ignore 的注释,它迫使该框架忽略掉一个特别的测试方法。也可以传入一条消息来向恰巧进行这项忽略测试的可信的开发人员传达您的决定。

@Ignore 注释

清单 7 展示了忽略掉一个正则表达式仍不起作用的测试是多么简单:

清单 7. 忽略这个测试
@Ignore("this regular expression isn't working yet")
@Test
public void verifyZipCodeMatch() throws Exception{		
 Pattern pattern = Pattern.compile("^\\d{5}([\\-]\\d{4})"); 
 Matcher mtcher = pattern.matcher("22011");
 boolean isValid = mtcher.matches();		
 assertTrue("Pattern did not validate zip code", isValid);
}

报告被忽略的测试

在 Eclipse 中运行这项测试会报告一项被忽略的测试,如图 1 所示:

图 1. Eclipse 中出现被忽略的测试
Eclipse 中出现被忽略的测试
Eclipse 中出现被忽略的测试

测试固件

测试固件并不是 JUnit 4 的新功能,但固件模型却是新的且改良过的。在本节中,我会解释为何及在何处需要使用固件,然后介绍旧版的不灵活固件与 JUnit 4 中大放异彩的新模型之间的区别。

为什么使用固件?

固件通过一个契约来倡导重用,该契约确保该特殊逻辑在测试之前或之后运行。在旧版的 JUnit 中,不管是否实现一个固件,这个契约都是隐式的。但 JUnit 4 却通过注释将固件显式化,这意味着只有在您真的决定使用固件时,该契约才成为强制的。

通过一个确保固件能在测试之前或之后运行的契约,可以编码可重用逻辑。例如,这种逻辑可能是初始化一个将在多个测试用例中测试的类,也可能是在运行一个数据依赖测试前填充数据库。不论是哪一种逻辑,使用固件都会确保一个更容易管理的测试用例:依赖于普通逻辑的测试用例。

当运行许多使用相同逻辑的测试,且一些测试或全部测试失败时,固件会变得特别方便。与其在每个测试设置的逻辑间切换,不如只在一个地方归纳导致失败的原因。除此之外,如果一些测试通过而另一些失败,您就能避免将该固件逻辑作为全部失败的源头来检查。

不灵活固件

旧版的 JUnit 使用一个在某种程度上不太灵活的固件模型,要求用 setUp()tearDown() 方法将每个测试方法包装起来。在清单 8 中可以看到该模型的一个潜在缺陷,其中实现了 setUp() 方法,因而运行了两次 —— 为每个定义的测试运行一次:

清单 8. 不灵活的固件
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import junit.framework.TestCase;

public class RegularExpressionTest extends TestCase {

 private String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
 private Pattern pattern;

 protected void setUp() throws Exception {
  this.pattern = Pattern.compile(this.zipRegEx);
 }

 public void testZipCodeGroup() throws Exception{		 
  Matcher mtcher = this.pattern.matcher("22101-5051");
  boolean isValid = mtcher.matches();			
  assertEquals("group(1) didn't equal -5051", "-5051", mtcher.group(1));
 }

 public void testZipCodeGroupException() throws Exception{		 
  Matcher mtcher = this.pattern.matcher("22101-5051");
  boolean isValid = mtcher.matches();			
  try{
   mtcher.group(2);
   fail("No exception was thrown");
  }catch(IndexOutOfBoundsException e){
  }
 }
}

围绕固件工作

在 JUnit 之前的版本中,使用 TestSetup 装饰器,指定一个固件只运行一次是可能的,但这是一个很麻烦的操作,如清单 9 所示(注意所要求的 suite() 方法):

清单 9. JUnit 4 之前版本的 TestSetup
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import junit.extensions.TestSetup;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import junit.textui.TestRunner;

public class OneTimeRegularExpressionTest extends TestCase {

 private static String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
 private static Pattern pattern;

 public static Test suite() {
  TestSetup setup = new TestSetup(
    new TestSuite(OneTimeRegularExpressionTest.class)) {
     protected void setUp() throws Exception {
      pattern = Pattern.compile(zipRegEx);
    }
   };
  return setup;
 }

 public void testZipCodeGroup() throws Exception {
  Matcher mtcher = pattern.matcher("22101-5051");
  boolean isValid = mtcher.matches();
  assertEquals("group(1) didn't equal -5051", "-5051", mtcher.group(1));
 }

 public void testZipCodeGroupException() throws Exception { 
  Matcher mtcher = pattern.matcher("22101-5051");
  boolean isValid = mtcher.matches();
  try {
   mtcher.group(2);
   fail("No exception was thrown");
  } catch (IndexOutOfBoundsException e) {
  }
 }
}

一句话,在 JUnit 4 之前,使用固件往往得不偿失。

4.0 版中的灵活性

JUnit 4 使用注释来减少固件花费的成本,允许为每个测试运行一次固件,或为整个类运行一次固件,或一次也不运行。有四种固件注释:针对类层次的固件有两种,针对方法层次的固件有两种。在类层次,有 @BeforeClass@AfterClass,在方法(或测试)层次,有 @Before@After

清单 10 中的测试用例包括一个使用 @Before 注释的固件,该固件针对两个测试运行:

清单 10. 使用注释的灵活固件
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;

public class RegularExpressionJUnit4Test {
 private static String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
 private static Pattern pattern;

 @Before
 public static void setUpBeforeClass() throws Exception {
  pattern = Pattern.compile(zipRegEx);
 }

 @Test
 public void verifyZipCodeNoMatch() throws Exception{		
  Matcher mtcher = this.pattern.matcher("2211");
  boolean notValid = mtcher.matches();		
  assertFalse("Pattern did validate zip code", notValid);
 }

 @Test(expected=IndexOutOfBoundsException.class)
 public void verifyZipCodeGroupException() throws Exception{		
  Matcher mtcher = this.pattern.matcher("22101-5051");
  boolean isValid = mtcher.matches();			
  mtcher.group(2);		
 }
}

一次性固件

如果只想运行一次固件会怎样呢?与其实现一个旧式的装饰器,如清单 9 所示,您不如使用 @BeforeClass 注释,如清单 11 所示:

清单 11. JUnit 4 中的一次性设置
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;

public class RegularExpressionJUnit4Test {
 private static String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
 private static Pattern pattern;

 @BeforeClass
 public static void setUpBeforeClass() throws Exception {
  pattern = Pattern.compile(zipRegEx);
 }

 @Test
 public void verifyZipCodeNoMatch() throws Exception{		
  Matcher mtcher = this.pattern.matcher("2211");
  boolean notValid = mtcher.matches();		
  assertFalse("Pattern did validate zip code", notValid);
 }

 @Test(expected=IndexOutOfBoundsException.class)
 public void verifyZipCodeGroupException() throws Exception{		
  Matcher mtcher = this.pattern.matcher("22101-5051");
  boolean isValid = mtcher.matches();			
  mtcher.group(2);		
 }
}

tearDown() 功能

为了防止您疑惑,顺便提一下,旧的 tearDown() 功能还没从新固件模型中去除。如果想要执行一个 tearDown(),只要创建一个新方法并根据需要使用 @After@AfterClass 即可。

灵活性的特例

可以为 JUnit 4 中的一个测试用例指定多个固件。新的注释驱动型固件并不阻止您创建多个 @BeforeClass 固件方法。请牢记,尽管如此,在当前的 JUnit 4 版本中,尚不能指定先运行哪个固件方法,这一点在您决定使用多个固件时就会变得十分棘手。

运行:在 JUnit 4 中测试

新的改进版 JUnit 4 最显著的特性是没有套件 —— 套件机制用于将测试从逻辑上分组并将这些测试作为一个单个单元来运行。在本节中,我会介绍替代了套件的新的精简注释,并介绍如何在 Eclipse 和 Ant 下运行 JUnit 4 测试。

旧式套件

找到清单 12 所示的旧式 JUnit 套件(这个套件将两个逻辑测试类分组,并将它们作为一个单个的单元运行),就能看到不同之处:

清单 12. 旧式 JUnit 套件
import junit.framework.Test;
import junit.framework.TestSuite;

public class JUnit3Suite {

 public static Test suite() {
  TestSuite suite = new TestSuite();
  suite.addTest(OneTimeRegularExpressionTest.suite());
  suite.addTestSuite(RegularExpressionTest.class);		
  return suite;
 }
}

两个新注释

在 JUnit 4 中,套件语义被两个新注释所替代。第一个是 @RunWith,设计它是为了方便让不同的运行器(除了构建进框架的运行器)执行一个特别的测试类。JUnit 4 绑定一个叫做 Suite 的套件运行器,必须在 @RunWith 注释中指定这个运行器。不仅如此,还必须提供另一项叫做 @SuiteClasses 的注释,它将一个意欲表示测试套件的类列表作为参数。

清单 13. 这些注释太棒了
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;

@RunWith(Suite.class)
@SuiteClasses({ParametricRegularExpressionTest.class,
      RegularExpressionTest.class,
      TimedRegularExpressionTest.class})
public class JUnit4Suite {

}

在 Eclipse 中运行 JUnit 4 测试

您可以选择通过如 Eclipse 的 IDE 或命令行来运行 JUnit 4 测试类。在 Eclipse 3.2 或以上版本中运行 JUnit 测试的方法是选择 Run As JUnit 测试选项。通过命令行运行测试要求执行 org.junit.runner.JUnitCore 类,并将完整有效的测试名作为参数传递。

例如,在 Eclipse 中,如果不想使用绑定的 JUnit 运行器,可以定义一个新运行配置,并首先指定 JUnitCore 类,如我在图 2 中所为:

图 2. 在 Eclipse 中运行 JUnit 4 命令行测试的第一步
在 Eclipse 中运行 JUnit 4 命令行测试的第一步
在 Eclipse 中运行 JUnit 4 命令行测试的第一步

指定一个测试

下一步,需要通过将完整有效的测试名添加到 Arguments 标签的 “Program arguments” 文本框中,来指定运行哪个测试,如图 3 所示:

图 3. 在 Eclipse 中运行 JUnit 命令行测试的第二步
在 Eclipse 中运行 JUnit 命令行测试的第二步
在 Eclipse 中运行 JUnit 命令行测试的第二步

Ant 和 JUnit 4

现在,Ant 和 JUnit 成为完美组合已久,许多开发人员预料这种关系在引入 JUnit 4 后只会变得更好。但结果是,存在一定问题。如果您正在运行 Ant 1.7 之前的任何版本,将不能轻易地运行现成的 JUnit 4 测试。那并不是说您不能运行这些测试 —— 而只是不能立刻运行这些测试。

不般配的一对

在 Ant(1.7 以前的版本)中运行 JUnit 4 测试(在清单 14 中)会产生一些有趣的结果;

清单 14. 一个简单的 JUnit 4 测试类
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertTrue;

public class RegularExpressionTest {
 private static String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
 private static Pattern pattern;

 @BeforeClass
 public static void setUpBeforeClass() throws Exception {
  pattern = Pattern.compile(zipRegEx);
 }

 @Test
 public void verifyGoodZipCode() throws Exception{		
  Matcher mtcher = this.pattern.matcher("22101");
  boolean isValid = mtcher.matches();		
  assertTrue("Pattern did not validate zip code", isValid);
 }
}

多处失败

在 Ant 中使用脆弱的 junit 任务会导致清单 15 中的错误:

清单 15. 一堆错误
[junit] Running test.com.acme.RegularExpressionTest
[junit] Tests run: 1, Failures: 1, Errors: 0, Time elapsed: 0.047 sec
[junit] Testsuite: test.com.acme.RegularExpressionTest
[junit] Tests run: 1, Failures: 1, Errors: 0, Time elapsed: 0.047 sec

[junit] Testcase: warning took 0.016 sec
[junit]     FAILED
[junit] No tests found in test.com.acme.RegularExpressionTest
[junit] junit.framework.AssertionFailedError: No tests found in
  test.com.acme.RegularExpressionTest
[junit] Test test.com.acme.RegularExpressionTest FAILED

相应的解决方案

如果想要在 Ant 1.7 版之前的版本上运行 JUnit 4 测试,必须用 suite() 方法来翻新测试用例,该方法返回一个 JUnit4TestAdapter 实例,如清单 16 所示:

清单 16. 旧方法的新用法
public static junit.framework.Test suite(){
 return new JUnit4TestAdapter(RegularExpressionTest.class);
}

由于和 @Test 注释的名称相似,所以必须让这个实例中的 Test 的返回类型名称完整。一旦 suite() 方法准备就绪,任何版本的 Ant 都会愉快地运行您的 JUnit 4 测试!

参数测试

偶尔,应用程序的业务逻辑要求您编写许多不定量的测试来保证其健壮。在 JUnit 之前的版本中,这种场景很不方便,主要是因为一个测试中方法的参数组各不相同,意味着要为每一个单独的组编写一个测试用例。

JUnit 4 引入了一项卓越的新功能,即能够创建由参数值供给的通用测试。结果是,您可以创建一个单个的测试用例并多次运行 —— 为您创建的每个参数运行一次。

参数的简洁性

在 JUnit 4 中创建参数测试只需要五个步骤:

  1. 创建一个不含参数的通用测试。
  2. 创建一个返回 Collection 类型的 static feeder 方法,并用 @Parameter 注释加以修饰。
  3. 为在步骤 1 中定义的通用方法所要求的参数类型创建类成员。
  4. 创建一个持有这些参数类型的构造函数,并把这些参数类型和步骤 3 中定义的类成员相应地联系起来。
  5. 通过 @RunWith 注释,指定测试用例和 Parameterized 类一起运行。

我将逐一介绍这些步骤。

步骤 1. 创建一个通用测试

清单 17 显示了验证一个正则表达式各种值的通用测试。注意 phrasematch 值未经定义。

清单 17. 一个通用测试
@Test
public void verifyGoodZipCode() throws Exception{		
 Matcher mtcher = this.pattern.matcher(phrase);
 boolean isValid = mtcher.matches();		
 assertEquals("Pattern did not validate zip code", isValid, match);
}

步骤 2. 创建一个 feeder 方法

下一步是创建一个 feeder 方法,必须将该方法声明为 static 并返回一个 Collection 类型。需要用 @Parameters 注释来修饰该方法。在该方法内部,仅仅创建一个多维 Object 数组,并将该数组转换为 List,如清单 18 所示:

清单 18. 含 @Parameters 注释的 feeder 方法
@Parameters
public static Collection regExValues() {
 return Arrays.asList(new Object[][] {
  {"22101", true },
  {"221x1", false },
  {"22101-5150", true },
  {"221015150", false }});
}

步骤 3. 创建两个类成员

由于这些参数是 Stringboolean 类型的,所以下一步要创建两个类成员:

清单 19. 声明两个类成员
private String phrase;
private boolean match;

步骤 4. 创建一个构造函数

接下来创建的构造函数将类成员和参数值联系起来,如清单 20 所示:

清单 20. 匹配值的构造函数
public ParametricRegularExpressionTest(String phrase, boolean match) {
 this.phrase = phrase;
 this.match = match;
}

步骤 5. 指定 Parameterized 类

最后,在类层次指定此测试必须和 Parameterized 类一起运行,如清单 21 所示:

清单 21. 指定 Parameterized 和 @RunWith 注释
@RunWith(Parameterized.class)
public class ParametricRegularExpressionTest {
 //...
}

运行测试

执行该测试类时,通用 verifyGoodZipCode() 测试方法运行四次,为定义在清单 18 的 regExValues() 数据 feeder 方法中的每个值对运行一次。

例如,如果在 Eclipse 下运行这个测试,会报告运行四个测试,如图 4 所示:

图 4. 在 Eclipse 中运行的参数测试
在 Eclipse 中运行的参数测试
在 Eclipse 中运行的参数测试

还有什么新功能?

除了到目前为止探讨过的重要改变,JUnit 4 还引入了一些小的功能;即,新断言方法的添加和一个终结状态的去除。

新断言

JUnit 4 添加了一个用于比较数组内容的新断言方法。这并不是什么重大举动,但它的确意味着您将不必再在数组内容间迭代,也不必再断言每个独立条目了。

例如,清单 22 中的代码在旧版的 JUnit 中是不可能的。这个测试失败是由于每个数组第二个元素中的一个微小的区别。

清单 22. JUnit 4 中的 assertEquals 现在支持数组
@Test
public void verifyArrayContents() throws Exception{
 String[] actual = new String[] {"JUnit 3.8.x", "JUnit 4", "TestNG"};
 String[] var = new String[] {"JUnit 3.8.x", "JUnit 4.1", "TestNG 5.5"};
 assertEquals("the two arrays should not be equal", actual, var);		
}

不再有错误!

JUnit 4 中一个虽然小但却很好的改变是它去除了错误的概念。之前的版本会既报告失败数,也报告错误数,在 JUnit 4 中,测试要么通过,要么失败。

有趣的是,去除了一个状态的同时,也添加了一个新状态,即与忽略测试功能相关的状态。当执行一系列测试时,JUnit 4 报告运行的测试数、失败的测试数和忽略掉的测试数。

结束语

JUnit 4 显著地背离了以往的设计并不意味着该框架执行起来和以往完全不同 —— 原有框架的强大功能和简洁性仍完好无损。事实上,如果深入发掘该框架,您会发现,尽管添加了一些强制性的新功能,但新框架并未牺牲任何曾引发开发人员测试革命的核心原则。

在本教程中,您亲历了了解 JUnit 4 的全过程,从测试声明到参数测试。您还发现了一些新功能,如超时测试和异常测试,了解了一些熟悉功能的改变,如固件和逻辑分组。您也看到了 Eclipse 中的测试是如何运行的,学习了一个允许在任何版本的 Ant 中(即使是 Ant 1.7 之前的版本)运行测试的简单方法。

在本教程中,如果有一件事是我希望您了解的,那就是:注释并没有削弱 JUnit 的功能,相反,它们的确带来了相当可观的易用性。试试注释吧:它们会让您在不知不觉中就能来去自如地驾驭测试的编写!


相关主题

  • 单元测试利器 JUnit 4” (苑永凯 ,developerWorks,2007 年 2 月):本文主要介绍了如何使用 JUnit 4 提供的各种功能开展有效的单元测试,并通过一个实例演示了如何使用 Ant 执行自动化的单元测试。
  • JUnit 4 抢先看” (Elliotte Rusty Harold,developerWorks,2005 年 9 月):Elliotte Harold 介绍了一下 JUnit 4。
  • 追求代码质量: JUnit 4 与 TestNG 的对比” (Andrew Glover,developerWorks,2006 年 8 月):JUnit 4 真的把 TestNG 的全部优势都借鉴过来了吗?
  • TestNG 使 Java 单元测试轻而易举” (Filippo Diotalevi,developerWorks,2005 年 1 月):从中不仅能看出 TestNG 的强大、开创性、可扩展和灵活;它还举例说明了一个有关 Java 注释的有趣的应用程序。
  • Tiger 中的注释,第 1 部分: 向 Java 代码中添加元数据” (Brett McLaughlin,developerWorks,2004 年 9 月):Brett McLaughlin 解释了元数据为什么如此有用,向您介绍了 Java 语言中的注释,并深入探讨了 Java 5 内嵌的注释。
  • 类处理工具包: 注释与配置文件” (Dennis Sosnoski,developerWorks,2005 年 8 月):Dennis Sosnoski 解释了为什么配置文件仍旧有用,特别是对于那些横穿应用程序源代码结构的类方面函数。
  • JUnit Reloaded” (Ralf Stuckert,Java.net,2006 年 12 月):将 JUnit 4 与之前的版本相比较。
  • JUnit 4 you” (Fabiano Cruz,Fabiano Cruz 的 Blog,2006 年 6 月):支持 JUnit 4 的工具和 IDE 概览。
  • Limiting asserts in test cases” (thediscoblog.com):针对 JUnit、TestNG 或其他极易失效的框架的最佳实践。
  • DbUnit with JUnit 4” (testearly.com):虽然 JUnit 4 是个新版本,但这并不意味着不能将它和为旧版的 JUnit 构建的扩展框架一起使用。
  • Using JUnit extensions in TestNG” (Andrew Glover,thediscoblog.com,2006 年 3 月):一个框架声明为 JUnit 扩展框架并不意味着它不能在 TestNG 中使用。
  • 追求代码质量 系列 (Andrew Glover,developerWorks):参阅此系列所有文章,从代码语法到测试框架到重构,涵盖的范围很广。

评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Open source
ArticleID=202782
ArticleTitle=深入探索 JUnit 4
publish-date=03202007