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