简而言之,JUnit:单元测试断言
簡而言之,本章涵蓋了各種單元測試斷言技術。 它詳細說明了內置機制, Hamcrest匹配器和AssertJ斷言的優缺點 。 正在進行的示例擴大了該主題,并說明了如何創建和使用自定義匹配器/斷言。
單元測試斷言
信任但要驗證
羅納德·里根(Ronald Reagan)
崗位測試結構解釋了為什么單元測試通常分階段進行。 它澄清說, 真正的測試即結果驗證在第三階段進行。 但是到目前為止,我們只看到了一些簡單的示例,主要使用了JUnit的內置機制。
如Hello World所示,驗證基于錯誤類型AssertionError 。 這是編寫所謂的自檢測試的基礎。 單元測試斷言將謂詞評估為true或false 。 如果為false ,則拋出AssertionError 。 JUnit運行時捕獲此錯誤并將測試報告為失敗。
以下各節將介紹三種較流行的單元測試斷言變體。
斷言
JUnit的內置斷言機制由類org.junit.Assert 。 它提供了兩種靜態方法來簡化測試驗證。 以下代碼片段概述了可用方法模式的用法:
fail(); fail( "Houston, We've Got a Problem." );assertNull( actual ); assertNull( "Identifier must not be null.",actual );assertTrue( counter.hasNext() ); assertTrue( "Counter should have a successor.",counter.hasNext() );assertEquals( LOWER_BOUND, actual ); assertEquals( "Number should be lower bound value.", LOWER_BOUND,actual );所有這些類型的方法都提供帶有String參數的重載版本。 如果發生故障,此參數將合并到斷言錯誤消息中。 許多人認為這有助于更清楚地指定失敗原因。 其他人則認為這樣的消息混亂,使測試更難以閱讀。
乍一看,這種單元測試斷言似乎很直觀。 這就是為什么我在前面的章節中使用它進行入門的原因。 此外,它仍然非常流行,并且工具很好地支持故障報告。 但是,在需要更復雜謂詞的斷言的表達性方面也受到一定限制。
Hamcrest
Hamcrest是一個旨在提供用于創建靈活的意圖表達的API的庫。 該實用程序提供了稱為Matcher的可嵌套謂詞。 這些允許以某種方式編寫復雜的驗證條件,許多開發人員認為比布爾運算符更易于閱讀。
MatcherAssert類支持單元測試斷言。 為此,它提供了靜態的assertThat(T, Matcher )方法。 傳遞的第一個參數是要驗證的值或對象。 第二個謂詞用于評估第一個謂詞。
assertThat( actual, equalTo( IN_RANGE_NUMBER ) );如您所見,匹配器方法模仿自然語言的流程以提高可讀性。 以下代碼片段更加清楚了此意圖。 這使用is(Matcher )方法來修飾實際的表達式。
assertThat( actual, is( equalTo( IN_RANGE_NUMBER ) ) );MatcherAssert.assertThat(...)存在另外兩個簽名。 首先,有一個采用布爾參數而不是Matcher參數的變量。 它的行為與Assert.assertTrue(boolean) 。
第二個變體將一個附加的String傳遞給該方法。 這可以用來提高故障消息的表達能力:
assertThat( "Actual number must not be equals to lower bound value.", actual, is( not( equalTo( LOWER_BOUND ) ) ) );在失敗的情況下,給定驗證的錯誤消息看起來像這樣:
Hamcrest帶有一組有用的匹配器。 圖書館在線文檔的“常見匹配項”部分列出了最重要的部分。 但是對于特定于域的問題,如果有合適的匹配器,通??梢蕴岣邌卧獪y試斷言的可讀性。
因此,該庫允許編寫自定義匹配器。
讓我們返回教程的示例來討論該主題。 首先,我們對該場景進行調整以使其更合理。 假設NumberRangeCounter.next()返回的是RangeNumber類型,而不是簡單的int值:
public class RangeNumber {private final String rangeIdentifier;private final int value;RangeNumber( String rangeIdentifier, int value ) {this.rangeIdentifier = rangeIdentifier;this.value = value;}public String getRangeIdentifier() {return rangeIdentifier;}public int getValue() {return value;} }我們可以使用自定義匹配器來檢查NumberRangeCounter#next()的返回值是否在計數器定義的數字范圍內:
RangeNumber actual = counter.next();assertThat( actual, is( inRangeOf( LOWER_BOUND, RANGE ) ) );適當的自定義匹配器可以擴展抽象類TypeSafeMatcher<T> 。 該基類處理null檢查和類型安全。 可能的實現如下所示。 請注意,它如何添加工廠方法inRangeOf(int,int)以便于使用:
public class InRangeMatcher extends TypeSafeMatcher<RangeNumber> {private final int lowerBound;private final int upperBound;InRangeMatcher( int lowerBound, int range ) {this.lowerBound = lowerBound;this.upperBound = lowerBound + range;}@Overridepublic void describeTo( Description description ) {String text = format( "between <%s> and <%s>.", lowerBound, upperBound );description.appendText( text );}@Overrideprotected void describeMismatchSafely(RangeNumber item, Description description ){description.appendText( "was " ).appendValue( item.getValue() );}@Overrideprotected boolean matchesSafely( RangeNumber toMatch ) {return lowerBound <= toMatch.getValue() && upperBound > toMatch.getValue();}public static Matcher<RangeNumber> inRangeOf( int lowerBound, int range ) {return new InRangeMatcher( lowerBound, range );} }對于給定的示例,工作量可能會有些夸大。 但它顯示了如何使用自定義匹配器消除先前帖子中有些神奇的IN_RANGE_NUMBER常量。 除了新類型外,還強制聲明語句的編譯時類型安全。 這意味著例如String參數將不被接受進行驗證。
下圖顯示了使用自定義匹配器的測試結果失敗的樣子:
很容易看出describeTo和describeMismatchSafely的實現以哪種方式影響故障消息。 它表示期望值應該在指定的下限和(計算的)上限1之間 ,并跟在實際值之后。
不幸的是,JUnit擴展了其Assert類的API以提供一組assertThat(…)方法。 這些方法實際上復制了MatcherAssert提供的API。 實際上,這些方法的實現委托給這種類型的相應方法。
盡管這似乎是一個小問題,但我認為值得一提。 由于這種方法,JUnit牢固地與Hamcrest庫綁定在一起。 這種依賴性有時會導致問題。 特別是與其他庫一起使用時,通過合并自己的hamcrest版本的副本,情況更糟……
Hamcrest的單元測試主張并非沒有競爭。 盡管關于每次測試一個確定與每個測試 一個概念的討論超出了本文的討論范圍,但后一種觀點的支持者可能認為該庫的驗證聲明過于嘈雜。 尤其是當一個概念需要多個斷言時。
這就是為什么我必須在本章中添加另一部分!
斷言
在“ 測試跑步者”中,示例片段之一使用了兩個assertXXX語句。 這些驗證期望的異常是IllegalArgumentException的實例并提供特定的錯誤消息。 該段看起來像這樣:
Throwable actual = ...assertTrue( actual instanceof IllegalArgumentException ); assertEquals( EXPECTED_ERROR_MESSAGE, actual.getMessage() );上一節教我們如何使用Hamcrest改進代碼。 但是,如果您碰巧是該庫的新手,您可能會想知道要使用哪個表達式。 或打字可能會感到不舒服。 無論如何,多個assertThat語句會加在一起。
AssertJ庫努力通過為Java提供流暢的斷言來改善這一點。 流暢的接口 API的目的是提供一種易于閱讀的,富有表現力的編程風格,從而減少膠合代碼并簡化鍵入。
那么如何使用這種方法來重構上面的代碼?
import static org.assertj.core.api.Assertions.assertThat;與其他方法類似,AssertJ提供了一個實用程序類,該類提供了一組靜態的assertThat方法。 但是這些方法針對給定的參數類型返回特定的斷言實現。 這就是所謂的語句鏈接的起點。
Throwable actual = ...assertThat( actual ).isInstanceOf( IllegalArgumentException.class ).hasMessage( EXPECTED_ERROR_MESSAGE );旁觀者認為可讀性在一定程度上可以擴展,但無論如何都可以用更緊湊的樣式來寫斷言。 了解如何流暢地添加與被測特定概念相關的各種驗證方面。 這種編程方法支持有效的類型輸入,因為IDE的內容輔助可以提供給定值類型的可用謂詞列表。
因此,您想向后世提供表現力的失敗消息嗎? 一種可能性是使用describedAs作為鏈中的第一個鏈接來注釋整個塊:
Throwable actual = ...assertThat( actual ).describedAs( "Expected exception does not match specification." ).hasMessage( EXPECTED_ERROR_MESSAGE ).isInstanceOf( NullPointerException.class );該代碼段期望使用NPE,但假設在運行時拋出了IAE。 然后失敗的測試運行將提供如下消息:
也許您希望根據給定的失敗原因使您的消息更加細微。 在這種情況下,您可以在每個驗證規范之前添加一條describedAs語句:
Throwable actual = ...assertThat( actual ).describedAs( "Message does not match specification." ).hasMessage( EXPECTED_ERROR_MESSAGE ).describedAs( "Exception type does not match specification." ).isInstanceOf( NullPointerException.class );還有更多的AssertJ功能可供探索。 但是,要使這篇文章保持在范圍之內,請參閱實用程序的在線文檔以獲取更多信息。 但是,在結束之前,讓我們再次看一下范圍內驗證示例。 這是可以通過自定義斷言解決的方法:
public class RangeCounterAssertionextends AbstractAssert<RangeCounterAssertion, RangeCounter> {private static final String ERR_IN_RANGE_OF = "Expected value to be between <%s> and <%s>, but was <%s>";private static final String ERR_RANGE_ID = "Expected range identifier to be <%s>, but was <%s>";public static RangeCounterAssertion assertThat( RangeCounter actual ) {return new RangeCounterAssertion( actual );}public InRangeAssertion hasRangeIdentifier( String expected ) {isNotNull();if( !actual.getRangeIdentifier().equals( expected ) ) {failWithMessage( ERR_RANGE_ID, expected, actual.getRangeIdentifier() );}return this;}public RangeCounterAssertion isInRangeOf( int lowerBound, int range ) {isNotNull();int upperBound = lowerBound + range;if( !isInInterval( lowerBound, upperBound ) ) {int actualValue = actual.getValue();failWithMessage( ERR_IN_RANGE_OF, lowerBound, upperBound, actualValue );}return this;}private boolean isInInterval( int lowerBound, int upperBound ) {return actual.getValue() >= lowerBound && actual.getValue() < upperBound;}private RangeCounterAssertion( Integer actual ) {super( actual, RangeCounterAssertion.class );} }自定義斷言是擴展AbstractAssert常見做法。 第一個通用參數是斷言的類型本身。 流利的鏈接樣式需要它。 第二種是斷言所基于的類型。
該實現提供了兩種附加的驗證方法,可以按照以下示例進行鏈接。 因此,這些方法將返回斷言實例本身。 請注意, isNotNull()的調用如何確保我們要在其上進行斷言的實際RangeNumber不為null 。
定制斷言由其工廠方法assertThat(RangeNumber) 。 因為它繼承了可用的基本檢查,所以斷言可以開箱即用地驗證非常復雜的規范。
RangeNumber first = ... RangeNumber second = ...assertThat( first ).isInRangeOf( LOWER_BOUND, RANGE ).hasRangeIdentifier( EXPECTED_RANGE_ID ).isNotSameAs( second );為了完整RangNumberAssertion ,以下是RangNumberAssertion的實際運行方式:
不幸的是,不可能在同一測試用例中將兩種不同的斷言類型與靜態導入一起使用。 當然,假定這些類型遵循assertThat(...)命名約定。 為了避免這種情況,文檔建議擴展實用程序類Assertions 。
這樣的擴展可用于提供靜態的assertThat方法,作為所有項目自定義斷言的入口。 通過在整個項目中使用此自定義實用程序類,不會發生導入沖突。 在為所有斷言提供單一入口點的部分中,可以找到詳細的描述:在線文檔中有關定制斷言的 yours + AssertJ 。
流利的API的另一個問題是單行鏈接的語句可能更難調試。 這是因為調試器可能無法在鏈中設置斷點。 此外,可能不清楚哪個方法調用已引起異常。
但是,正如Wikipedia所說的那樣,可以通過將語句分成多行來克服這些問題,如上面的示例所示。 這樣,用戶可以在鏈中設置斷點,并輕松地逐行逐步執行代碼。
結論
簡而言之,JUnit的這一章介紹了不同的單元測試斷言方法,例如該工具的內置機制,Hamcrest匹配器和AssertJ斷言。 它概述了一些優缺點,并通過本教程的進行中示例對主題進行了擴展。 此外,還展示了如何創建和使用自定義匹配器和斷言。
盡管基于Assert的機制肯定是過時的并且不太面向對象,但它仍然具有它的提倡者。 Hamcrest匹配器將斷言和謂詞定義完全分開,而AssertJ斷言以緊湊且易于使用的編程風格進行評分。 所以現在您選擇太多了……
請注意,這將是本教程有關JUnit測試要點的最后一章。 這并不意味著沒有更多要說的了。 恰恰相反! 但這將超出此迷你系列量身定制的范圍。 您知道他們在說什么: 總是讓他們想要更多…
翻譯自: https://www.javacodegeeks.com/2014/09/junit-in-a-nutshell-unit-test-assertion.html
總結
以上是生活随笔為你收集整理的简而言之,JUnit:单元测试断言的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: HttpURLConnection的警告
- 下一篇: nba电脑版需要什么显卡(nba电脑版游