還有 assertAll 方法,它接受可變數(shù)量的斷言作為參數(shù),并保證它們?nèi)康玫綀?zhí)行,然后再把錯(cuò)誤信息(如果有)一并匯報(bào)出來(lái)。
@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>
這個(gè)特性在檢查對(duì)象的多個(gè)屬性值時(shí)非常有用。按照一般的做法,測(cè)試在第一個(gè)斷言失敗時(shí)會(huì)掛掉了,此時(shí)只有第一個(gè)出錯(cuò)的地方得到提示,而你無(wú)法得知其他值的斷言是否成功,只好再跑一遍測(cè)試。
后,我們終于有了 assertThrows 和 expectThrows 方法。兩者均會(huì)在被測(cè)方法未拋出預(yù)期異常時(shí)失敗。而后者還會(huì)返回拋出的異常實(shí)例,以用于后續(xù)的驗(yàn)證,比如,斷言異常信息包含正確的信息等。
@Test
void assertExceptions() {
assertThrows(Exception.class, this::throwing);
Exception exception = expectThrows(Exception.class, this::throwing);
assertEquals("Because I can!", exception.getMessage());
}
假言/判定(Assumptions)
假言/判定允許你僅在特定條件滿足時(shí)才運(yùn)行測(cè)試。這個(gè)特性能夠減少測(cè)試組件的運(yùn)行時(shí)間和代碼重復(fù),特別是在假言都不滿足的情況下。
@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)
);
}
假言/判定適用于兩種情形,要么是你希望在某些條件不滿足時(shí)中止測(cè)試,要么是你希望僅當(dāng)某個(gè)條件滿足時(shí)才執(zhí)行(部分)測(cè)試。主要的區(qū)別是,被中止的測(cè)試是以被禁用(disabled)的形式被報(bào)告,此時(shí)沒(méi)有測(cè)試任何內(nèi)容,因?yàn)闂l件得不到滿足。
測(cè)試嵌套
在 JUnit 5 中,嵌套測(cè)試幾乎不費(fèi)吹灰之力。你只需要在嵌套的類上添加 @Nested 注解,類中的所有方法即會(huì)被引擎執(zhí)行:
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);
}
}
}
}
如你所見(jiàn),嵌套類中的 @BeforeEach(及 @AfterEach )注解也工作良好。不過(guò),構(gòu)造順序似乎還未被寫入文檔,它們的初始化次序是從外向內(nèi)的。這也讓你能疊加式地為內(nèi)部類準(zhǔn)備測(cè)試數(shù)據(jù)。
如果嵌套的內(nèi)部測(cè)試想要存取外部測(cè)試類的字段,那么嵌套類本身不應(yīng)該是靜態(tài)的。但這樣一來(lái)也禁止了靜態(tài)方法的使用,因而這種場(chǎng)景下@BeforeAll 和 @AfterAll 方法也無(wú)法使用了(還是說(shuō)終有他法實(shí)現(xiàn)?)
你可能有疑惑,嵌套的內(nèi)部測(cè)試類有什么用。個(gè)人而言,我用內(nèi)部類來(lái)漸進(jìn)測(cè)試接口,其他人則多用于保持測(cè)試類短小專注。后者同時(shí)也有一個(gè)經(jīng)典的例子來(lái)說(shuō)明,例子由 JUnit 團(tuán)隊(duì)提供,它測(cè)試了一個(gè)棧:
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...
}
}
}
在上面的例子中,棧的狀態(tài)改變會(huì)反映到內(nèi)層的測(cè)試類中,其中內(nèi)部類又基于自身的場(chǎng)景執(zhí)行了一些測(cè)試。
測(cè)試命名
JUnit 5 提供了一個(gè)注解 @DisplayName,它用以為開發(fā)者提供更可讀的測(cè)試類和測(cè)試方法信息。
上面的 stack 測(cè)試?yán)蛹由显撟⒔庖院笞兂蛇@樣:
@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 看了會(huì)感動(dòng),BDDer 看了會(huì)流淚的測(cè)試結(jié)果輸出。
![](/ckfinder/userfiles/images/2016092953305984.png)
回顧
差不多這些了,恭喜你終于讀完了。我們匆匆過(guò)完了 JUnit 5 的基本特性,現(xiàn)在,你應(yīng)該了解了所有寫測(cè)試的必備知識(shí)了:包括如何為方法添加生命周期注解(@[Before|After][All|Each]、如何注解測(cè)試方法本身(@Test)、如何嵌套測(cè)試(@Nested)、如何給測(cè)試一個(gè)好信息(@DisplayName),你也應(yīng)該能了解斷言和假言判定是如何工作的了(基本上與前版無(wú)異)。
不過(guò)這可還沒(méi)完!我們還沒(méi)聊到 測(cè)試方法的條件執(zhí)行,沒(méi)聊到非?岬 參數(shù)注入 ,以及 JUnit 5 的擴(kuò)展機(jī)制 和 架構(gòu)體系 呢。放心,這真的是后了,這些話題我們會(huì)一個(gè)月后再聊,現(xiàn)在你可以先休息一下啦。