本文作者鲍震渊,广通软件高级工程师,入行码农十余年经验,专注于CRM系统的设计与开发,16年开启APM的研究工作。广通软件十多年来耕耘于运维管理软件研发和服务咨询,面向数据中心、互联网、物联网三个领域提供整合化的运维工具和服务。微信公众号: broada_ops
JUnit对Java开发者来说是一个在普遍使用的单元测试框架。但是,大多数使用者仅仅使用了它的一部分功能。本文介绍其中不常被用到的8个功能。比如临时目录创建及自动清理,测试失败时所有断言的执行,失控测试的强制停止等。
@Rule是JUnit4的新特性。利用@Rule注解我们可以扩展JUnit的功能,在执行case的时候加入测试者特有的操作,而不影响原有的case代码:减小了特有操作和case原逻辑的耦合。譬如说我们要重复测试某个test方法时,当然我们可以在@Test方法里面写循环。但是如果想把循环和测试逻辑分开就可以利用@Rule。
@Rule只能注解在字段中,该字段必须是public的并且类型必须实现了TestRule接口或MethodRule接口。JUnit 4.9还导入了@ClassRule注解。相对于@Rule注解来说,@ClassRule是一个类级别的注解。就像@Before与@BeforeClass的区别。下面介绍JUnit中8个不常用到但很有用的功能。
通过@Rule注解可以生成临时文件或临时文件夹
有时候程序运行时必须生成文件或文件夹,往往需要写代码。JUnit4.7以后的版本中引入了TemporaryFolder。测试可以创建文件与目录并在测试运行结束后将其删除。这对于那些与文件系统打交道而且独立运行的测试来说很有用。这个功能使测试中文件夹的生成和管理变的非常简单。只要确保生成的文件夹或文件是名字不与既有的重复就行。并且,无论测试成功与否,任何在测试过程中添加到临时目录的文件或目录都会在测试结束时删除。
下面的例子中,JUnit生成的临时目录中生成一个sample.txt的文件,并且写入「JUnit Rocks!」。然后,读取新生成文件的第一行并进行比较。为使代码简化,例子中文件处理使用了Apache Commons IO。
public class TemporaryFolderTests {
@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();
@Test
public void fileCreatedAndWrittenSuccessfully() throws IOException {
File file = tempFolder.newFile("sample.txt");
FileUtils.writeStringToFile(file, "JUnit Rocks!");
String line = FileUtils.readFileToString(file);
assertThat(line, is("JUnit Rocks!"));
}
}
测试完后(无论测试成功与否),文件和目录会被删除。
取得当前的测试方法名称
想要取得执行中的测试方法名的时候,通过@Rule注解TestName类的实例化对象可以取得。
public class TestNameTests {
@Rule
public TestName name = new TestName();
@Test
public void methodNameShouldBePrinted() {
System.out.println("Test method name:" + name.getMethodName());
}
}
通过getMethodName()方法可以得到当前测试方法名称。
上面例子的执行结果是:
Test method name: methodNameShouldBePrinted
在一个运行测试方法的过程中收集多个错误信息
使用ErrorCollector类,可以在一个测试方法方法中收集多个测试错误。也就是说,一个测试方法执行中,不会在第一个确认出错后就停止执行。使用ErrorCollector可以在所有点确认完后统一报出。
下在例子中有三个点要确认,它们都用ErrorCollector来确认。
public class ErrorCollectorTests {
@Rule
public ErrorCollector collector = new ErrorCollector();
@Test
public void statementsCollectedSuccessfully() {
String s = null;
collector.checkThat ("Value should not be null", null, is(s));
s = "";
collector.checkThat( "Value should have the length of 1", s.length(), is(1));
s = "Junit!";
collector.checkThat( "Value should have the length of 10", s.length(), is(10));
}
}
第一个点确认成功,后面两个点都失败的时候会报下面这样的错。
java.lang.AssertionError:Value should have the length of 1
Expected: is <1>
but: was <0>
at org.hamcrest.MatcherAssert...
java.lang.AssertionError:Value should have the length of 10
Expected: is <10>
but: was <6>
at org.hamcrest.MatcherAssert...
一个测试方法用不同的参数来反复执行
用不同的参数来测试一个方法是否正确是一个比较普遍的需求。JUnit4.0以后的版本中,通过@Parameters注解可以不重复写测试方法就能实现这一需求。
使用@Parameters注解时不能使用默认Runner来运行测试代码,必须使用Parameterized.class测试类。
通过下面的例子来说明一下。
@RunWith(Parameterized.class)
public class FibonacciNumbersTests {
@Parameterized.Parameters
public static List data() {
return Arrays.asList(new Object[][]{
{0, 0}, {1, 1}, {2, 1},
{3, 2}, {4, 3}, {5, 5},
{6, 8}});
}
private int value;
private int expected;
public FibonacciNumbersTests( int input, int expected) {
value = input;
this.expected = expected;
}
@Test
public void fibonacciNumberCalc () {
assertEquals(expected, fib(value));
}
public static int fib(int n) {
if (n < 2) {
return n;
} else {
return fib(n - 1) + fib(n - 2);
}
}
}
第一行指定@RunWith注解,JUnit用专门的Runner类来运行测试代码。
List的各项目中第一个是input参数,第二个是expected参数。
测试方法通过@Parameterized.Parameters注解,把List的各个项目做为测试类的构造函数的参数。
设定执行最长时间
通过@Test注解中的timeout属性可以使执行时间过长的测试方法强制结束掉,同时测试结果也表示为测试失败。timeout属性中设定的时间是毫秒单位的。并且可以设定整个类的执行最长时间。下面例子中设定了5秒最长执行时间。
public class LongRunningTests {
@Rule
public Timeout globalTimeout = Timeout.seconds(5);
@Test
public void whatWeDoInATestMethodEchoesInEternity() {
while(true);
}
}
执行结果如下:
org.junit.runners.model.TestTimedOutException:
test timed out after 5 seconds
at tr.com.t2.labs.tdd.sample5.LongRunningTests…
用@Category把测试分组
JUnit 4.8以后的版本中,加入了分组测试的功能。下面例6的两个测试方法中thisTestRunsSlowly()加上了@Category注解。
public class CategorizedTests {
@Test
@Category(SlowTests.class)
public void thisTestRunsSlowly() {
System.out.println("Slow test running");
}
@Test
public void thisTestRunsFast() {
System.out.println("Fast test running");
}
}
SlowTests.class的定义是下面这样的。
public interface SlowTests {
}
在例7中用Categories.class的Runner来运行分组后的测试代码。指定类的Runner没有设定的话@Category注解是无效的。用@IncludeCategory注解来指定要执行的接口。这样只有指定的分组才会被执行到。
@RunWith(Categories.class)
@Categories.IncludeCategory(SlowTests.class)
@Suite.SuiteClasses(CategorizedTests.class)
public class SlowTestsTestSuite {
}
自定义测试规则
通过实现TestRule接口可以简单的实现自定义测试规则。TestRule中只定义了apply()方法。在实现apply()方法时返回一个Statement类型的返回值。Statement类是定义了evaluate()方法的抽象类。具体参照以下例子。
public class MyCustomRule implements TestRule {
private String label;
public MyCustomRule(String label) {
this.label = label;
}
@Override
public Statement apply(final Statement base, Description description){
return new Statement() {
@Override
public void evaluate() throws Throwable {
System.out.println(label + " before");
base.evaluate();
System.out.println(label + " after");
}
};
}
}
下面例9中引入了例8的自定义规则。
public class CustomRuleTests {
@Rule
public MyCustomRule myCustomRule = new MyCustomRule("custom");
@Test
public void myAwesomeMethodInvokedSuccessfully() {
System.out.println("Test worked OK");
}
}
执行结果如下。
custom before
Test worked OK
custom after
使用RuleChain
RuleChain提供一种将多个TestRule串在一起执行的机制。这在JUnit 4.10以后的版本中可以使用。需要根据特定顺序执行多个处理的时候,用RuleChain可以提高效率。
下面例10是在例9上写的一个例子。
public class RuleChainTests {
@Rule
public RuleChain chain = RuleChain.outerRule(
new MyCustomRule("outer")).around(new MyCustomRule("inner")
);
@Test
public void ruleChainWorkedOK() {
System.out.println("Test worked OK");
}
}
执行如下:
outer before
inner before
Test worked OK
inner after
outer after