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