简而言之,JUnit:测试隔离
作為顧問,我仍然經(jīng)常遇到程序員,他們對(duì)JUnit及其正確用法的理解最多。 這使我有了編寫多部分教程的想法,從我的角度解釋了要點(diǎn)。
盡管存在一些有關(guān)使用該工具進(jìn)行測試的好書和文章,但是也許可以通過本動(dòng)手實(shí)踐的方法來使一兩個(gè)額外的開發(fā)人員對(duì)單元測試感興趣,這將使他們值得付出努力。
注意,本章的重點(diǎn)是基本的單元測試技術(shù),而不是JUnit功能或API。 在后面的帖子中將討論更多后者。 用于描述這些技術(shù)的術(shù)語是基于Meszaros的xUnit測試模式 [MES]中提供的定義。
以前簡而言之在JUnit上
本教程從“ Hello World”一章開始,介紹了測試的基本知識(shí):如何編寫,執(zhí)行和評(píng)估它。 它繼續(xù)進(jìn)行后期測試結(jié)構(gòu) ,解釋了通常用于構(gòu)建單元測試的四個(gè)階段(設(shè)置,練習(xí),驗(yàn)證和拆卸)。
這些課程還附有一個(gè)一致的示例,以使抽象概念更易于理解。 它被證明了,一個(gè)測試用例是如何一點(diǎn)一點(diǎn)地增長的-從幸福的道路開始到極端的案例測試,包括預(yù)期的例外。
總的來說,要強(qiáng)調(diào)的是,測試不僅是簡單的驗(yàn)證機(jī),還可以作為一種低級(jí)規(guī)范。 因此,應(yīng)該以人們可能想到的最高編碼標(biāo)準(zhǔn)來開發(fā)它。
依存關(guān)系
一個(gè)巴掌拍不響
諺語
本教程中使用的示例都是關(guān)于編寫一個(gè)簡單的數(shù)字范圍計(jì)數(shù)器,該計(jì)數(shù)器從給定值開始傳遞一定數(shù)量的連續(xù)整數(shù)。 指定單元行為的測試用例可能在摘錄中看起來像這樣:
public class NumberRangeCounterTest {private static final int LOWER_BOUND = 1000;private static final int RANGE = 1000;private static final int ZERO_RANGE = 0;private NumberRangeCounter counter= new NumberRangeCounter( LOWER_BOUND, RANGE );@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 );}@Test( expected = IllegalStateException.class )public void exeedsRange() {new NumberRangeCounter( LOWER_BOUND, ZERO_RANGE ).next();}[...] }注意,這里我使用了一個(gè)非常緊湊的測試用例,以節(jié)省空間,例如使用隱式夾具設(shè)置和異常驗(yàn)證。 有關(guān)測試結(jié)構(gòu)化模式的詳細(xì)討論,請參見上一章 。
還要注意,我堅(jiān)持使用JUnit內(nèi)置功能進(jìn)行驗(yàn)證。 我將在另一篇文章中介紹特定匹配器庫( Hamcrest , AssertJ )的優(yōu)缺點(diǎn)。
雖然NumberRangeCounter的初始描述足以使本教程開始,但細(xì)心的讀者可能已經(jīng)注意到,該方法顯然有些幼稚。 例如,考慮程序的進(jìn)程可能會(huì)終止。 為了能夠在系統(tǒng)重新啟動(dòng)時(shí)正確地重新初始化計(jì)數(shù)器,它至少應(yīng)保留其最新狀態(tài)。
但是,保持計(jì)數(shù)器的狀態(tài)涉及通過不屬于單元(也就是被測系統(tǒng)(SUT))的軟件組件(數(shù)據(jù)庫驅(qū)動(dòng)程序,文件系統(tǒng)API等)訪問資源(數(shù)據(jù)庫,文件系統(tǒng)等)。 這意味著單位取決于這些組件,Meszaros用術(shù)語“ 依賴組件”(DOC)描述 。
不幸的是,這在許多方面帶來了與測試有關(guān)的麻煩:
那么,我們該如何解決這個(gè)問題呢?
隔離–單元測試員的SEP字段
所謂SEP是我們不能看,或者不看,還是我們的大腦并沒有讓我們看到的,因?yàn)槲覀冋J(rèn)為這公司的S omebody?LSE是P&roblem ...。
福特長官
由于我們不希望單元測試依賴于DOC的行為,也不希望它們過慢或脆弱,因此我們努力使我們的單元盡可能不受軟件所有其他部分的影響。 簡單地說,我們使這些特殊問題成為其他測試類型的關(guān)注點(diǎn)-因此開玩笑的SEP Field報(bào)價(jià)。
通常,此原理稱為SUT隔離,表示希望分別測試關(guān)注點(diǎn)并保持測試彼此獨(dú)立 。 實(shí)際上,這意味著應(yīng)該以一種可以將每個(gè)DOC替換為所謂的Test Double的方式來設(shè)計(jì)單元, Test Double是Test [MES1]的輕量級(jí)替代組件。
與我們的示例相關(guān),我們可能決定不直接從單元本身內(nèi)部訪問數(shù)據(jù)庫,文件系統(tǒng)等。 相反,我們可以選擇將此問題分為屏蔽接口類型,而不必關(guān)心具體實(shí)現(xiàn)的外觀。
盡管從低級(jí)設(shè)計(jì)的角度來看,這種選擇當(dāng)然也是合理的,但它并不能說明在整個(gè)測試過程中如何創(chuàng)建,安裝和使用雙重測試。 但是在詳細(xì)介紹如何使用雙打之前,還有一個(gè)主題需要討論。
間接輸入和輸出
到目前為止,我們的測試工作僅以SUT的直接輸入和輸出面對(duì)我們。 即, NumberRangeCounter每個(gè)實(shí)例都配有一個(gè)下限和一個(gè)范圍值(直接輸入)。 并且在每次調(diào)用next() ,SUT返回一個(gè)值或引發(fā)一個(gè)異常(直接輸出),用于驗(yàn)證SUT的預(yù)期行為。
但是現(xiàn)在情況變得更加復(fù)雜了。 考慮到DOC為SUT初始化提供了最新的計(jì)數(shù)器值, next()的結(jié)果取決于該值。 如果DOC以這種方式提供SUT輸入,我們將討論間接輸入 。
相反,假設(shè)next()每次調(diào)用都應(yīng)保持計(jì)數(shù)器的當(dāng)前狀態(tài),則我們沒有機(jī)會(huì)通過SUT的直接輸出進(jìn)行驗(yàn)證。 但是我們可以檢查計(jì)數(shù)器的狀態(tài)是否已委托給DOC。 這種委托稱為間接輸出 。
有了這些新知識(shí),我們應(yīng)該準(zhǔn)備繼續(xù)進(jìn)行NumberRangeCounter示例。
使用存根控制間接輸入
從我們學(xué)到的知識(shí)來看,將計(jì)數(shù)器的狀態(tài)保存分為自己的類型可能是個(gè)好主意。 這種類型會(huì)將SUT與實(shí)際的存儲(chǔ)實(shí)現(xiàn)隔離開來,因?yàn)閺腟UT的角度來看,我們對(duì)如何實(shí)際解決保留問題不感興趣。 因此,我們引入了CounterStorage接口。
盡管到目前為止還沒有真正的存儲(chǔ)實(shí)現(xiàn),但我們可以使用測試倍數(shù)來代替。 由于接口尚無方法,因此此時(shí)創(chuàng)建測試雙重類型很簡單。
public class CounterStorageDouble implements CounterStorage { }為了以松散耦合的方式為NumberRangeCounter提供存儲(chǔ),我們可以使用依賴注入 。 通過兩次存儲(chǔ)測試來增強(qiáng)隱式夾具設(shè)置,然后將其注入到SUT中,如下所示:
private CounterStorage storage;@Beforepublic void setUp() {storage = new CounterStorageDouble();counter = new NumberRangeCounter( storage, LOWER_BOUND, RANGE );}修復(fù)編譯錯(cuò)誤并運(yùn)行所有測試后,該欄應(yīng)保持綠色,因?yàn)槲覀兩形锤娜魏涡袨椤?但是現(xiàn)在我們希望對(duì)NumberRangeCounter#next()的第一次調(diào)用尊重存儲(chǔ)的狀態(tài)。 如果存儲(chǔ)提供的值n在計(jì)數(shù)器的定義范圍內(nèi),則next()的第一次調(diào)用也應(yīng)返回n ,這由以下測試表示:
private static final int IN_RANGE_NUMBER = LOWER_BOUND + RANGE / 2;[...]@Testpublic void initialNumberFromStorage() {storage.setNumber( IN_RANGE_NUMBER );int actual = counter.next();assertEquals( IN_RANGE_NUMBER, actual );}我們的測試IN_RANGE_NUMBER必須提供確定性的間接輸入,在我們的情況下為IN_RANGE_NUMBER 。 因此,它使用setNumber(int)來配備值。 但是由于尚未使用存儲(chǔ),因此測試失敗。 要更改此設(shè)置,是時(shí)候聲明CounterStorage的第一個(gè)方法了:
public interface CounterStorage {int getNumber(); }這使我們可以像這樣實(shí)現(xiàn)雙重測試:
public class CounterStorageDouble implements CounterStorage {private int number;public void setNumber( int number ) {this.number = number;}@Override public int getNumber() {return number;} }如您所見,double通過返回由setNumber(int)饋送的配置值來實(shí)現(xiàn)getNumber() setNumber(int) 。 以這種方式提供間接輸入的測試雙稱為存根 。 現(xiàn)在,我們將能夠?qū)崿F(xiàn)NumberRangeCounter的預(yù)期行為并通過測試。
如果您認(rèn)為get / setNumber用不好的名字來描述存儲(chǔ)的行為,我同意。 但這簡化了職位的演變。 請感到受邀提出構(gòu)思周到的重構(gòu)建議…
間諜的間接輸出驗(yàn)證
為了能夠在系統(tǒng)重啟后恢復(fù)NumberRangeCounter實(shí)例,我們希望計(jì)數(shù)器的每個(gè)狀態(tài)更改都將保留。 這可以通過在每次調(diào)用next()時(shí)將當(dāng)前狀態(tài)分配到存儲(chǔ)中來實(shí)現(xiàn)。 因此,我們向DOC類型添加了一個(gè)setNumber(int)方法:
public interface CounterStorage {int getNumber();void setNumber( int number ); }新方法與用于配置存根的簽名具有相同的簽名,這真是一個(gè)奇怪的巧合! 在使用@Override修改該方法后,很容易將我們的夾具設(shè)置重新用于以下測試:
@Testpublic void storageOfStateChange() {counter.next();assertEquals( LOWER_BOUND + 1, storage.getNumber() );}與初始狀態(tài)相比,我們期望在調(diào)用next()之后,計(jì)數(shù)器的新狀態(tài)將增加一個(gè)。 更重要的是,我們希望這個(gè)新狀態(tài)作為間接輸出傳遞到存儲(chǔ)DOC。 不幸的是,我們沒有看到實(shí)際的調(diào)用,因此我們在double的局部變量中記錄了調(diào)用的結(jié)果。
如果記錄的值與預(yù)期值相匹配,則驗(yàn)證階段將推斷出正確的間接輸出已傳遞到DOC。 上面以最簡單的方式描述的記錄狀態(tài)和/或行為以供以后驗(yàn)證,也稱為間諜。 因此,使用這種技術(shù)的測試兩倍被稱為間諜 。
那Mo子呢?
還有一種可能通過使用模擬來驗(yàn)證next()的間接輸出。 這種類型的double的最重要的特征是,間接輸出驗(yàn)證是在委托方法內(nèi)部執(zhí)行的。 此外,它還可以確保實(shí)際調(diào)用了預(yù)期的方法:
public class CounterStorageMock implements CounterStorage {private int expectedNumber;private boolean done;public CounterStorageMock( int expectedNumber ) {this.expectedNumber = expectedNumber;}@Overridepublic void setNumber( int actualNumber ) {assertEquals( expectedNumber, actualNumber );done = true;}public void verify() {assertTrue( done );}@Overridepublic int getNumber() {return 0;} }CounterStorageMock實(shí)例通過構(gòu)造函數(shù)參數(shù)配置了期望值。 如果setNumber(int) ,則立即檢查給定值是否與預(yù)期值匹配。 一個(gè)標(biāo)志存儲(chǔ)該方法已被調(diào)用的信息。 這允許使用verify()方法檢查實(shí)際的調(diào)用。
這就是使用模擬的storageOfStateChange測試的外觀:
@Testpublic void storageOfStateChange() {CounterStorageMock storage= new CounterStorageMock( LOWER_BOUND + 1 );NumberRangeCounter counter= new NumberRangeCounter( storage, LOWER_BOUND, RANGE );counter.next();storage.verify();}如您所見,測試中沒有剩下任何規(guī)格驗(yàn)證。 通常的測試結(jié)構(gòu)有些扭曲,這似乎很奇怪。 這是因?yàn)轵?yàn)證條件是在夾具設(shè)置中間的運(yùn)動(dòng)階段之前指定的。 驗(yàn)證階段僅保留模擬調(diào)用檢查。
但是作為回報(bào),模擬可以在行為驗(yàn)證失敗的情況下提供精確的堆棧跟蹤,這可以簡化問題分析。 如果再次查看間諜解決方案,您將認(rèn)識(shí)到失敗跟蹤只會(huì)指向測試的驗(yàn)證部分。 沒有關(guān)于實(shí)際上導(dǎo)致測試失敗的生產(chǎn)代碼行的信息。
這與模擬完全不同。 跟蹤將使我們能夠準(zhǔn)確識(shí)別setNumber(int)調(diào)用位置。 有了這些信息,我們可以輕松地設(shè)置斷點(diǎn)并調(diào)試問題。
由于這篇文章的范圍,我只限于對(duì)存根,間諜和模擬進(jìn)行雙重測試。 有關(guān)其他類型的簡短說明,您可以查看Martin Fowler的文章TestDouble ,但是可以在Meszaros的xUnit測試模式書[MES]中找到所有類型及其變型的深入說明。
在Tomek Kaczanowski的書《 使用JUnit和Mockito [KAC]進(jìn)行實(shí)際單元測試 》中可以找到基于測試雙重框架的模擬與間諜的良好比較(請參閱下一節(jié))。
閱讀本節(jié)后,您可能會(huì)覺得編寫所有這些測試雙打是繁瑣的工作。 毫不奇怪,已編寫庫來大大簡化雙重處理。
測試雙重框架–應(yīng)許之地?
如果您只有錘子,那么一切看起來就像釘子
諺語
開發(fā)了一些框架以簡化使用測試雙打的任務(wù)。 不幸的是,就精確的測試雙重術(shù)語而言,這些庫并不總是一件好事。 例如, JMock和EasyMock專注于模擬 ,而Mockito卻以間諜為中心。 也許這就是為什么大多數(shù)人都在談?wù)摮靶?,而不管他們實(shí)際上在使用哪種類型的雙人間。
但是,有跡象表明 ,Mockito當(dāng)時(shí)是首選的雙重測試工具。 我猜這是因?yàn)樗峁┝肆己玫拈喿x流利的接口API,并通過提供詳細(xì)的驗(yàn)證失敗消息來彌補(bǔ)上述間諜提及的缺點(diǎn)。
無需詳細(xì)介紹,我提供了一個(gè)storageOfStateChange()測試版本,該版本使用Mockito進(jìn)行間諜創(chuàng)建和測試驗(yàn)證。 請注意, mock和verify是Mockito類型的靜態(tài)方法。 通常的做法是將靜態(tài)導(dǎo)入與Mockito表達(dá)式一起使用以提高可讀性:
@Testpublic void storageOfStateChange() {CounterStorage storage = mock( CounterStorage.class );NumberRangeCounter counter = new NumberRangeCounter( storage, LOWER_BOUND, RANGE );counter.next();verify( storage ).setNumber( LOWER_BOUND + 1 );}關(guān)于是否使用此類工具的文章很多。 例如,羅伯特·C·馬丁(Robert C. Martin) 更喜歡手寫雙打 ,邁克爾·博爾迪沙(Michael Boldischar)甚至認(rèn)為嘲笑框架有害 。 在我看來,后者只是在簡單地濫用 ,而我一次不同意馬丁所說的“寫那些嘲笑是微不足道的”。
在發(fā)現(xiàn)Mockito之前,我多年來一直在使用手寫雙打。 立刻,我被賣給了流利的存根語法 (一種直觀的驗(yàn)證方式),我認(rèn)為擺脫那些笨拙的雙精度類型是一種改進(jìn)。 但這當(dāng)然是情人眼中的。
但是,我經(jīng)歷了雙重測試工具的誘惑,誘使開發(fā)人員過度操作。 例如,用雙倍替換第三方組件非常容易,否則創(chuàng)建起來可能會(huì)很昂貴。 但這被認(rèn)為是不好的做法, 史蒂夫·弗里曼 ( Steve Freeman)和納特·普賴斯 ( Nat Pryce )詳細(xì)解釋了為什么只應(yīng)模擬自己擁有的類型 [FRE_PRY]。
第三方代碼要求進(jìn)行集成測試和抽象適配器層 。 后者實(shí)際上就是我們在示例中通過引入CounterStorage所指示的內(nèi)容。 而且,由于我們擁有適配器,因此可以安全地將其替換為雙適配器。
一個(gè)容易進(jìn)入的第二個(gè)陷阱是編寫測試,其中一個(gè)測試雙精度返回另一個(gè)測試雙精度。 如果到了這一點(diǎn),您應(yīng)該重新考慮正在使用的代碼的設(shè)計(jì)。 這可能會(huì)破壞demeter的定律 ,這意味著對(duì)象耦合在一起的方式可能有問題。
最后但并非最不重要的一點(diǎn)是,如果您考慮使用雙重測試框架,則應(yīng)記住,這通常是影響整個(gè)團(tuán)隊(duì)的長期決策。 由于代碼風(fēng)格的一致性,混合使用不同的框架可能不是最好的主意,即使您僅使用一種框架,每個(gè)(新)成員也必須學(xué)習(xí)特定于工具的API。
在開始廣泛使用雙打測試之前,您可能會(huì)考慮閱讀比較經(jīng)典測試與模擬測試的馬丁·福勒的“ 莫克斯不是存根” ,或羅伯特·C·馬丁的“ 何時(shí)模擬” ,其中介紹了一些啟發(fā)式方法,以找出雙打和太多之間的黃金比例。加倍。 或如Tomek Kaczanowski所說:
“很高興您可以嘲笑一切,是嗎? 放慢速度,并確保您確實(shí)需要驗(yàn)證交互。 你可能沒有。 [KAC1]
結(jié)論
簡而言之,JUnit的這一章討論了單元依賴性對(duì)測試的影響。 它說明了隔離的原理,并說明了如何通過用測試雙倍替換DOC來將其付諸實(shí)踐。 在這種情況下,提出了間接輸入和輸出的概念,并描述了其與測試的相關(guān)性。
該示例通過動(dòng)手示例加深了知識(shí),并介紹了幾種測試double類型及其使用目的。 最后,簡短介紹了測試雙重框架及其優(yōu)缺點(diǎn),從而結(jié)束了本章。 希望它具有足夠的平衡性,可以使您對(duì)該主題有一個(gè)全面的了解,而又不致于瑣碎。 改進(jìn)建議當(dāng)然受到高度贊賞。
本教程的下一篇文章將介紹Runners和Rules等JUnit功能,并通過正在進(jìn)行的示例展示如何使用它們。
參考文獻(xiàn)
[MES] xUnit測試模式,Gerard Meszaros,2007年
[MES1] xUnit測試模式,第5章,原理:隔離SUT,Gerard Meszaros,2007年
[KAC]使用JUnit和Mockito進(jìn)行實(shí)用單元測試,附錄C。TestSpy vs. Mock,Tomek Kaczanowski,2013年
[KAC1]錯(cuò)誤測試,良好測試,第4章,可維護(hù)性,托梅克·卡扎諾夫斯基,2013年
[FRE_PRY]不斷增長的面向?qū)ο筌浖?#xff0c;由測試指導(dǎo),第8章,史蒂夫·弗里曼(Steve Freeman),納特·普萊斯(Nat Pryce),2010年
翻譯自: https://www.javacodegeeks.com/2014/09/junit-in-a-nutshell-test-isolation.html
總結(jié)
以上是生活随笔為你收集整理的简而言之,JUnit:测试隔离的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 卡夫卡编年史队列基准
- 下一篇: 手机与电脑轻松互联如何将手机和电脑连接