还有 assertAll 方法,它接受可变数量的断言作为参数,并保证它们全部得到执行,然后再把错误信息(如果有)一并汇报出来。
@Test
void assertAllProperties() {
Address address = new Address("New City", "Some Street", "No");
assertAll("address",
() -> assertEquals("Neustadt", address.city),
() -> assertEquals("Irgendeinestra?e", address.street),
() -> assertEquals("Nr", address.number)
);
}
org.opentest4j.MultipleFailuresError: address (3 failures)
expected: <Neustadt> but was: <New City>
expected: <Irgendeinestra?e> but was: <Some Street>
expected: <Nr> but was: <No>
这个特性在检查对象的多个属性值时非常有用。按照一般的做法,测试在第一个断言失败时就会挂掉了,此时只有第一个出错的地方得到提示,而你无法得知其他值的断言是否成功,只好再跑一遍测试。
最后,我们终于有了 assertThrows 和 expectThrows 方法。两者均会在被测方法未抛出预期异常时失败。而后者还会返回抛出的异常实例,以用于后续的验证,比如,断言异常信息包含正确的信息等。
@Test
void assertExceptions() {
assertThrows(Exception.class, this::throwing);
Exception exception = expectThrows(Exception.class, this::throwing);
assertEquals("Because I can!", exception.getMessage());
}
假言/判定(Assumptions)
假言/判定允许你仅在特定条件满足时才运行测试。这个特性能够减少测试组件的运行时间和代码重复,特别是在假言都不满足的情况下。
@Test void exitIfFalseIsTrue() { assumeTrue(false); System.exit(1); } @Test void exitIfTrueIsFalse() { assumeFalse(this::truism); System.exit(1); } private boolean truism() { return true; } @Test void exitIfNullEqualsString() { assumingThat( "null".equals(null), () -> System.exit(1) ); } |
假言/判定适用于两种情形,要么是你希望在某些条件不满足时中止测试,要么是你希望仅当某个条件满足时才执行(部分)测试。主要的区别是,被中止的测试是以被禁用(disabled)的形式被报告,此时没有测试任何内容,因为条件得不到满足。
测试嵌套
在 JUnit 5 中,嵌套测试几乎不费吹灰之力。你只需要在嵌套的类上添加 @Nested 注解,类中的所有方法即会被引擎执行:
package org.codefx.demo.junit5; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; class Nest { int count = Integer.MIN_VALUE; @BeforeEach void setCountToZero() { count = 0; } @Test void countIsZero() { assertEquals(0, count); } @Nested class CountGreaterZero { @BeforeEach void increaseCount() { count++; } @Test void countIsGreaterZero() { assertTrue(count > 0); } @Nested class CountMuchGreaterZero { @BeforeEach void increaseCount() { count += Integer.MAX_VALUE / 2; } @Test void countIsLarge() { assertTrue(count > Integer.MAX_VALUE / 2); } } } } |
如你所见,嵌套类中的 @BeforeEach(及 @AfterEach )注解也工作良好。不过,构造顺序似乎还未被写入文档,它们的初始化次序是从外向内的。这也让你能叠加式地为内部类准备测试数据。
如果嵌套的内部测试想要存取外部测试类的字段,那么嵌套类本身不应该是静态的。但这样一来也就禁止了静态方法的使用,因而这种场景下@BeforeAll 和 @AfterAll 方法也就无法使用了(还是说终有他法实现?)
你可能有疑惑,嵌套的内部测试类有什么用。个人而言,我用内部类来渐进测试接口,其他人则多用于保持测试类短小专注。后者同时也有一个经典的例子来说明,例子由 JUnit 团队提供,它测试了一个栈:
class TestingAStack { Stack<Object> stack; boolean isRun = false; @Test void isInstantiatedWithNew() { new Stack<Object>(); } @Nested class WhenNew { @BeforeEach void init() { stack = new Stack<Object>(); } // some tests on 'stack', which is empty @Nested class AfterPushing { String anElement = "an element"; @BeforeEach void init() { stack.push(anElement); } // some tests on 'stack', which has one element... } } } |
在上面的例子中,栈的状态改变会反映到内层的测试类中,其中内部类又基于自身的场景执行了一些测试。
测试命名
JUnit 5 提供了一个注解 @DisplayName,它用以为开发者提供更可读的测试类和测试方法信息。
上面的 stack 测试例子加上该注解以后就变成这样:
@DisplayName("A stack") class TestingAStack { @Test @DisplayName("is instantiated with new Stack()") void isInstantiatedWithNew() { /*...*/ } @Nested @DisplayName("when new") class WhenNew { @Test @DisplayName("is empty") void isEmpty() { /*...*/ } @Test @DisplayName("throws EmptyStackException when popped") void throwsExceptionWhenPopped() { /*...*/ } @Test @DisplayName("throws EmptyStackException when peeked") void throwsExceptionWhenPeeked() { /*...*/ } @Nested @DisplayName("after pushing an element") class AfterPushing { @Test @DisplayName("it is no longer empty") void isEmpty() { /*...*/ } @Test @DisplayName("returns the element when popped and is empty") void returnElementWhenPopped() { /*...*/ } @Test @DisplayName( "returns the element when peeked but remains not empty") void returnElementWhenPeeked(){ /*...*/ } } } } |
这是一份TDDer 看了会感动,BDDer 看了会流泪的测试结果输出。
回顾
差不多就这些了,恭喜你终于读完了。我们匆匆过完了 JUnit 5 的基本特性,现在,你应该了解了所有写测试的必备知识了:包括如何为方法添加生命周期注解(@[Before|After][All|Each]、如何注解测试方法本身(@Test)、如何嵌套测试(@Nested)、如何给测试一个好信息(@DisplayName),你也应该能了解断言和假言判定是如何工作的了(基本上与前版无异)。
不过这可还没完!我们还没聊到 测试方法的条件执行,没聊到非常酷的 参数注入 ,以及 JUnit 5 的扩展机制 和 架构体系 呢。放心,这真的是最后了,这些话题我们会一个月后再聊,现在你可以先休息一下啦。