简而言之,JUnit:测试结构
盡管存在關(guān)于JUnit測(cè)試的書(shū)籍和文章,但我仍然經(jīng)常遇到程序員,他們至多對(duì)這個(gè)工具及其正確用法都不甚了解。 因此,我想到了編寫(xiě)多部分教程的想法,從我的角度解釋了要點(diǎn)。
也許在這個(gè)小型系列中采用的動(dòng)手方法可能適合使一兩個(gè)額外的開(kāi)發(fā)人員對(duì)單元測(cè)試感興趣,這將使工作值得。
上次我介紹了測(cè)試的基本知識(shí)–測(cè)試的編寫(xiě),執(zhí)行和評(píng)估方式。 在這樣做的同時(shí),我概述了測(cè)試不僅僅是一個(gè)簡(jiǎn)單的驗(yàn)證機(jī),而且還可以用作一種低級(jí)規(guī)范。 因此,應(yīng)該以人們可能想到的最高編碼標(biāo)準(zhǔn)來(lái)開(kāi)發(fā)它。
這篇文章將繼續(xù)本教程的示例,并使用Meszaros在xUnit Test Patterns [MES]中定義的命名法,得出表征良好編寫(xiě)的單元測(cè)試的通用結(jié)構(gòu)。
測(cè)試的四個(gè)階段
整潔的房子,整潔的頭腦
老格言
本教程的示例是關(guān)于編寫(xiě)一個(gè)簡(jiǎn)單的數(shù)字范圍計(jì)數(shù)器,該計(jì)數(shù)器從給定值開(kāi)始提供一定數(shù)量的連續(xù)整數(shù)。 從快樂(lè)的路徑開(kāi)始,最后一個(gè)帖子的結(jié)果是一個(gè)測(cè)試,該測(cè)試已驗(yàn)證, NumberRangeCounter在后續(xù)調(diào)用next方法時(shí)返回連續(xù)數(shù)字:
@Testpublic void subsequentNumber() { NumberRangeCounter counter = new NumberRangeCounter();int first = counter.next();int second = counter.next();assertEquals( first + 1, second );}請(qǐng)注意,本章將堅(jiān)持使用JUnit內(nèi)置功能進(jìn)行驗(yàn)證。 我將在另一篇文章中介紹特定匹配器庫(kù)( Hamcrest , AssertJ )的優(yōu)缺點(diǎn)。
細(xì)心的讀者可能已經(jīng)注意到,我使用空行將測(cè)試分為不同的部分,并且可能想知道為什么。 為了回答這個(gè)問(wèn)題,讓我們更仔細(xì)地研究三個(gè)部分:
這種測(cè)試結(jié)構(gòu)非常普遍,并已被多位作者描述。 它被標(biāo)記為排列,執(zhí)行,聲明 [KAC] –或構(gòu)建,操作,檢查 [MAR2] –模式。 但是,對(duì)于本教程,我想精確一點(diǎn)并堅(jiān)持使用Meszaros的[MES]這四個(gè)階段,分別是 設(shè)置(1),練習(xí)(2),驗(yàn)證(3)和拆卸(4) 。
普通單元測(cè)試很少使用持久性?shī)A具??,因此拆卸階段(如我們的示例所示)通常被省略。 而且由于它與規(guī)范角度完全不相關(guān),因此無(wú)論如何我們都希望將其排除在測(cè)試方法之外。 一分鐘內(nèi)將介紹如何實(shí)現(xiàn)此目的。
由于這篇文章的范圍,我避免了單元測(cè)試的精確定義。 但是,我堅(jiān)持Tomek Kaczanowski在使用JUnit和Mockito進(jìn)行實(shí)用單元測(cè)試中描述的三種類(lèi)型的開(kāi)發(fā)人員測(cè)試 ,可以概括為:
- 單元測(cè)試可確保您的代碼正常運(yùn)行,并且必須經(jīng)常運(yùn)行,因此運(yùn)行速度非常快。 基本上,這就是本教程的全部?jī)?nèi)容。
- 集成測(cè)試關(guān)注于不同模塊的正確集成,包括開(kāi)發(fā)人員無(wú)法控制的代碼。 這通常需要一些資源(例如數(shù)據(jù)庫(kù),文件系統(tǒng)),因此測(cè)試運(yùn)行速度較慢。
- 端到端測(cè)試從客戶(hù)端的角度驗(yàn)證您的代碼是否有效,并將系統(tǒng)作為一個(gè)整體進(jìn)行測(cè)試,以模仿用戶(hù)的使用方式。 他們通常需要大量時(shí)間才能執(zhí)行自己。
- 對(duì)于如何有效地組合這些測(cè)試類(lèi)型的深入示例,您可以看看Steve Freeman和Nat Pryce的 Tests指導(dǎo)的Growinging Oriented Oriented Software 。
但是在繼續(xù)進(jìn)行示例之前,還有一個(gè)問(wèn)題需要討論:
為什么這很重要?
閱讀(代碼)與寫(xiě)作所花費(fèi)的時(shí)間比例遠(yuǎn)遠(yuǎn)超過(guò)10:1…
羅伯特·馬丁
四個(gè)階段模式的目的是使您易于理解測(cè)試正在驗(yàn)證的行為。 安裝程序始終定義測(cè)試的前提條件,練習(xí)實(shí)際上會(huì)調(diào)用測(cè)試的行為,驗(yàn)證是否指定了預(yù)期的結(jié)果,而拆除工作完全與內(nèi)部維護(hù)有關(guān),正如Meszaros所說(shuō)的那樣。
這種干凈的相分離清楚地表明了單個(gè)測(cè)試的意圖,并提高了可讀性。 該方法意味著測(cè)試一次只能驗(yàn)證給定輸入狀態(tài)的一種行為,因此通常沒(méi)有條件塊等(單條件測(cè)試)。
試圖避免繁瑣的夾具安裝并在單一方法中測(cè)試盡可能多的功能雖然很誘人,但這通常會(huì)導(dǎo)致某種性質(zhì)的混淆 。 因此,請(qǐng)始終記住:如果不小心編寫(xiě)測(cè)試,可能會(huì)給維護(hù)和進(jìn)步帶來(lái)痛苦。
但是現(xiàn)在是時(shí)候繼續(xù)進(jìn)行示例了,看看這種新知識(shí)可以為我們做什么!
角落案例測(cè)試
完成快樂(lè)路徑測(cè)試后,我們將繼續(xù)指定極端情況行為。 對(duì)數(shù)字范圍計(jì)數(shù)器的描述指出,數(shù)字序列應(yīng)從給定值開(kāi)始。 這一點(diǎn)很重要,因?yàn)樗x了計(jì)數(shù)器范圍的下限(一個(gè)角…)。
將該值作為配置參數(shù)傳遞給NumberRangeCounter的構(gòu)造函數(shù)似乎很合理。 適當(dāng)?shù)臏y(cè)試可以驗(yàn)證next返回的第一個(gè)數(shù)字是否等于此初始化:
@Testpublic void lowerBound() {NumberRangeCounter counter = new NumberRangeCounter( 1000 );int actual = counter.next();assertEquals( 1000, actual );}再次,我們的測(cè)試類(lèi)不會(huì)編譯。 通過(guò)將lowerBound參數(shù)引入計(jì)數(shù)器的構(gòu)造函數(shù)來(lái)解決此問(wèn)題,則會(huì)在subsequentNumber測(cè)試中導(dǎo)致編譯錯(cuò)誤。 幸運(yùn)的是,后一個(gè)測(cè)試被編寫(xiě)為獨(dú)立于下限定義,因此該測(cè)試的夾具也可以使用該參數(shù)。
但是,測(cè)試中的原義數(shù)字是多余的,沒(méi)有明確指出其目的。 后者通常表示為幻數(shù) 。 為了改善這種情況,我們可以引入一個(gè)常量LOWER_BOUND并替換所有文字值。 以下是測(cè)試類(lèi)的外觀:
public class NumberRangeCounterTest {private static final int LOWER_BOUND = 1000;@Testpublic void subsequentNumber() {NumberRangeCounter counter = new NumberRangeCounter( LOWER_BOUND );int first = counter.next();int second = counter.next();assertEquals( first + 1, second );}@Testpublic void lowerBound() {NumberRangeCounter counter = new NumberRangeCounter( LOWER_BOUND );int actual = counter.next();assertEquals( LOWER_BOUND, actual );} }查看代碼,您可能會(huì)注意到夾具的在線設(shè)置對(duì)于兩種測(cè)試都是相同的。 通常,內(nèi)聯(lián)設(shè)置由多個(gè)語(yǔ)句組成,但是測(cè)試之間通常存在共同點(diǎn)。 為了避免冗余,可以將共同之處委托給設(shè)置方法:
public class NumberRangeCounterTest {private static final int LOWER_BOUND = 1000;@Testpublic void subsequentNumber() {NumberRangeCounter counter = setUp();int first = counter.next();int second = counter.next();assertEquals( first + 1, second );}@Testpublic void lowerBound() {NumberRangeCounter counter = setUp();int actual = counter.next();assertEquals( LOWER_BOUND, actual );}private NumberRangeCounter setUp() {return new NumberRangeCounter( LOWER_BOUND );} }如果委托設(shè)置方法可以提高給定情況下的可讀性,這是有爭(zhēng)議的,但它會(huì)導(dǎo)致JUnit的一個(gè)有趣功能: 隱式執(zhí)行公共測(cè)試設(shè)置的可能性。 這可以通過(guò)將@Before注釋?xiě)?yīng)用于不帶返回值和參數(shù)的公共非靜態(tài)方法來(lái)實(shí)現(xiàn)。
這意味著此功能需要付出一定的代價(jià)。 如果要消除測(cè)試中的多余setUp調(diào)用,則必須引入一個(gè)采用NumberRangeCounter實(shí)例的字段:
public class NumberRangeCounterTest {private static final int LOWER_BOUND = 1000;private NumberRangeCounter counter;@Beforepublic void setUp() {counter = new NumberRangeCounter( LOWER_BOUND );}@Testpublic void subsequentNumber() {int first = counter.next();int second = counter.next();assertEquals( first + 1, second );}@Testpublic void lowerBound() {int actual = counter.next();assertEquals( LOWER_BOUND, actual );} }顯而易見(jiàn), 隱式設(shè)置可以消除很多代碼重復(fù)。 但是從測(cè)試的角度來(lái)看,它也引入了一種魔術(shù),這會(huì)使閱讀變得困難。 因此,對(duì)于“我應(yīng)該使用哪種安裝類(lèi)型?”這個(gè)問(wèn)題,答案很明確。 是:這取決于…
由于我通常會(huì)注意保持單位/測(cè)試較小,因此折衷似乎可以接受。 因此,我經(jīng)常使用隱式設(shè)置來(lái)定義公共/快樂(lè)路徑輸入,并為每個(gè)極端案例測(cè)試通過(guò)小的內(nèi)聯(lián)/代理設(shè)置對(duì)它進(jìn)行相應(yīng)的補(bǔ)充。 否則,由于特別是初學(xué)者傾向于讓測(cè)試變得更大,因此最好堅(jiān)持使用內(nèi)聯(lián)和委托設(shè)置。
JUnit運(yùn)行時(shí)確保在測(cè)試類(lèi)的新實(shí)例上調(diào)用每個(gè)測(cè)試。 這意味著在我們的示例中,僅構(gòu)造函數(shù)的燈具可以完全省略setUp方法。 可以通過(guò)隱式方式為counter字段分配新的 fixture:
private NumberRangeCounter counter = new NumberRangeCounter( LOWER_BOUND );盡管有些人@Before使用它,但其他人則認(rèn)為@Before注釋方法會(huì)使意圖更加明確。 好吧,我不會(huì)就此進(jìn)行戰(zhàn)爭(zhēng),讓您自己決定的決定……
隱式拆解
想象一下,無(wú)論出于何種原因都需要處理NumberRangeCounter 。 這意味著我們必須在測(cè)試中添加拆卸階段。 根據(jù)我們的最新代碼片段,使用JUnit可以輕松實(shí)現(xiàn),因?yàn)樗С质褂?#64;After注釋進(jìn)行隱式拆卸 。 我們只需要添加以下方法:
@Afterpublic void tearDown() {counter.dispose();}如上所述,拆卸完全是關(guān)于客房清潔的,完全不對(duì)特定測(cè)試添加任何信息。 因此,隱式執(zhí)行此操作通常很方便。 或者,即使測(cè)試失敗,也必須使用try-finally構(gòu)造來(lái)處理此問(wèn)題,以確保執(zhí)行拆解。 但是后者通常不會(huì)提高可讀性。
預(yù)期的例外
一個(gè)特殊的極端情況是測(cè)試預(yù)期的異常。 出于示例考慮,如果next的調(diào)用超出給定范圍的值量,則NumberRangeCalculator應(yīng)該引發(fā)IllegalStateException 。 同樣,通過(guò)構(gòu)造函數(shù)參數(shù)配置范圍可能是合理的。 使用try-catch構(gòu)造,我們可以編寫(xiě):
@Testpublic void exeedsRange() {NumberRangeCounter counter = new NumberRangeCounter( LOWER_BOUND, 0 );try {counter.next();fail();} catch( IllegalStateException expected ) {}}好吧,這看起來(lái)有些丑陋,因?yàn)樗:藴y(cè)試階段的分離,并且可讀性不強(qiáng)。 但是由于Assert.fail()會(huì)引發(fā)AssertionError因此可以確保在沒(méi)有引發(fā)異常的情況下測(cè)試失敗。 并且catch塊可以確保在拋出預(yù)期異常的情況下成功完成測(cè)試。
使用Java 8,可以使用lambda表達(dá)式編寫(xiě)結(jié)構(gòu)清晰的異常測(cè)試。 有關(guān)更多信息,請(qǐng)參閱
使用Java 8 Lambdas清潔JUnit Throwable-Tests 。
如果足以驗(yàn)證是否已拋出某種類(lèi)型的異常,則JUnit通過(guò)@Test批注的expected方法提供隱式驗(yàn)證 。 上面的測(cè)試可以寫(xiě)成:
@Test( expected = IllegalStateException.class )public void exeedsRange() {new NumberRangeCounter( LOWER_BOUND, ZERO_RANGE ).next();}盡管此方法非常緊湊,但也很危險(xiǎn)。 這是因?yàn)椴荒軈^(qū)分是在設(shè)置的建立階段還是在測(cè)試的執(zhí)行階段拋出了給定的異常。 因此,如果構(gòu)造函數(shù)意外IllegalStateException則測(cè)試將是綠色的,因此毫無(wú)價(jià)值。
JUnit提供了第三種可能性,可以更清晰地測(cè)試預(yù)期異常,即ExpectedException規(guī)則。 由于我們還沒(méi)有涵蓋規(guī)則 ,并且該方法有點(diǎn)扭曲了四個(gè)階段的結(jié)構(gòu),因此我將對(duì)該主題的明確討論推遲到有關(guān)規(guī)則和運(yùn)行者的后續(xù)文章上,并且僅提供摘要作為預(yù)告片:
public class NumberRangeCounterTest {private static final int LOWER_BOUND = 1000; @Rulepublic ExpectedException thrown = ExpectedException.none();@Testpublic void exeedsRange() {thrown.expect( IllegalStateException.class );new NumberRangeCounter( LOWER_BOUND, 0 ).next();}[...] }但是,如果您不想等待,可以在Rafa?Borowiec的 《 JUNIT EXPECTEDEXCEPTION RULE:BEYOND BASICS 》一文中詳細(xì)了解一下。
結(jié)論
簡(jiǎn)而言之,JUnit的這一章解釋了通常用于編寫(xiě)單元測(cè)試的四個(gè)階段結(jié)構(gòu)-設(shè)置,練習(xí),驗(yàn)證和拆卸。 它描述了每個(gè)階段的目的,并著重強(qiáng)調(diào)了在一致使用時(shí)如何提高測(cè)試用例的可讀性。 該示例在極端案例測(cè)試的上下文中加深了該學(xué)習(xí)材料。 希望它具有足夠的平衡性,可以提供容易理解的介紹而又不瑣碎。 改進(jìn)建議當(dāng)然受到高度贊賞。
本教程的下一章將繼續(xù)該示例,并介紹如何處理單元依賴(lài)性和測(cè)試隔離,敬請(qǐng)關(guān)注。
參考文獻(xiàn)
- [MES] xUnit測(cè)試模式,第19章,四階段測(cè)試,Gerard Meszaros,2007年
- [MAR1]清潔規(guī)范,第9章:單元測(cè)試,第130頁(yè)及以下,Robert C. Martin,2009年
- [KAC]使用JUnit和Mockito進(jìn)行的實(shí)用單元測(cè)試,3.9。 單元測(cè)試的階段,Tomek Kaczanowski,2013年
- [MAR2]清潔代碼,第9章:單元測(cè)試,第127頁(yè),Robert C. Martin,2009年
翻譯自: https://www.javacodegeeks.com/2014/08/junit-in-a-nutshell-test-structure.html
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來(lái)咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)總結(jié)
以上是生活随笔為你收集整理的简而言之,JUnit:测试结构的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 中信银行薪金煲怎么查看普通赎回的明细?
- 下一篇: 95599短信查询开户行