keep the bar green to keep the code clean.
單元測試,是保證軟體品質和效率的重要手段之一。能點進來看本文的,都是有品質追求的同學哈,這裡不對單元測試的必要性作贅述,簡單提一下單元測試的五點好處:
監測軟體品質
提升項目效率
促進代碼優化
增加重構自信
軟體行為文檔化
本文書寫的目的,是期望對目前單元測試相關技術做一個梳理和總結,在寫法上給一些示例,幫助同學對單元測試相關的工具有大體了解,并能根據示例寫出合理的單元測試。
單元測試書寫的基本順序大緻包括:定義測試類、标記測試方法、建立被測試執行個體、執行被測試方法、結果驗證。這個過程中有三個難點:
Runner。
這裡不把Runner了解為JUnit的運作器,這裡了解為單元測試的基礎架構。架構的目的是為了幫助我們做資源加載等事情,定制單元測試的模闆,讓我們能夠專注于單測case本身的書寫。是以選擇一個優秀的測試架構是必要的。
我們常用的架構如:JUnit4,TestNG,IntlTest(IntlTestBlockJUnit4ClassRunner),springTest & springbootTest & SpringContainer4Test(SpringJUnit4ClassRunner)等。
Mocks。
Mocks無疑是為了讓我們更穩定的運作單元測試,它能隔離環境和外部資料對單元測試的影響,使得結果可預測。更重要的一點:mocks屏蔽了其他代碼塊對被測試塊的影響,使得我們做的是真正的”單元“代碼的測試。
是以就這個點而言,我個人是主張分層&純内部邏輯測試的,舉兩個例子說明:
常用的Mock工具:JMockit,此外還有EasyMock、jMock、Mockito、PowerMock等。
Assert
這裡把assert也拿出來說一說,是因為我個人之前一直使用junit自帶的Assert工具做斷言,對于Hamcrest有一些了解,它自身封裝了很多比對器,也更加貼近自然語言,是以結合Junit的Assert能夠在一定程度上支援複雜邏輯的斷言。但是用過AssertJ以後,就決定隻用它了。另外對專門做json斷言的JSONassert也會做一些介紹。
// TODO By 4.15
優點:它是基礎的assert工具,能滿足全部斷言需求。提供的api為assertTrue/assertEquals/assertNull/assertSame/assertArrayEquals等。其中assertThat(T actual, Matcher<? super T> matcher) 方法提供了一個可擴充接口。
缺點:JUnit的assert不對各類資料類型做邏輯封裝(如String#substring()類似方法要自己調用解決),是以複雜資料類型或者複雜邏輯的校驗,一方面需要我們自己實作斷言的内容,另一方面要拆成多個獨立的斷言處理。是以使用起來比較麻煩,代碼看上去也比較臃腫,語義不夠直覺。
定義。首先說hamcrest自身并不是一個單元測試架構,它本質上是一個包含很多有用的比對器的庫。可以使用在很多場景,尤其适合用于對:org.junit.Assert.assertThat(T actual, Matcher<? super T> matcher)做比對功能的擴充,是以可以配合一起使用。
優點。
語義更加貼近自然語言,易于了解;
起源于java,另外多種語言提供支援,如Java, C++, Objective-C, Python, PHP, Ruby, Swift等。
缺點
不支援流式檢查,對于一個結果做多元度的判斷仍需要拆分斷言;
api不夠豐富,很多領域對象沒有對應方法,如Date,Exception等;
發展較慢,對新技術的支援不到位。
api。下圖為常用比對器總覽。
由于assert工具在這裡推薦AssertJ,是以例子放多一些。
這是個其他斷言沒有的一個特色功能,是以說明一下。普通的單測方法在第一個檢查失敗時就結束跳出。SoftAssert提供了一個全部執行的功能,即全部斷言都會運作,并列印失敗結果。
優點:從上面一些demo中不難看出AssertJ的以下優點。
流式斷言,對一個對象可以根據需要在一行代碼中使用api接連斷言,代碼量少且優雅;
api可讀性更好,更加貼近自然語義,AssertJ中封裝了海量的api,基本都可以從名字中明确了解含義;
api庫更強大。除了以上基礎類型和異常、日期、類屬性、soft斷言api,更突出的優勢是擴充了對以下領域的支援:DB(據說适配myBatis, Hibernate, JOOQ等多種DB架構)、Guava、Swing;Uri、xml、file;jdb8如:Future,Stream, Optional, Java 8 Date等。
開源&免費,對新技術支援迅速。目前許多新技術都
缺點:待考察。
定義:JSONassert是個很輕量的工具包,封裝處理了JSONObject、JSON String、JSONArray的比較邏輯。旨在:讓開發者對json的單測寫更少的代碼,并且适合做REST interfaces的測試。
優點:JSONassert會将string轉換為JSONObject,并且結合對象的邏輯結構和資料做比較。它提供了兩個次元的比較選擇:是否容忍資料順序不一緻(推薦),是否容忍資料擴充,即可以選擇:被比較對象增加了部分屬性(忽略比較),隻比較相同的屬性部分。(JSONassert converts your string into a JSON object and compares the logical structure and data with the actual JSON. When strict is set to false (recommended), it forgives reordering data and extending results (as long as all the expected elements are there), making tests less brittle.)
缺點:個人覺得JSONCompareMode了解性上不是太好哈,而且compare傳回的是個Result Pojo,需要自己用"_success"屬性判斷是否成功,封裝性待考慮哈。跟AssertJ的api的設計還是有差距的。不過目前看json的斷言比較好的工具就是JSONassert,大家可以自己體驗下。
從以上介紹的順序也能看出assert工具的一個逐漸進化的過程:
api從計算機的表達方式逐漸轉為自然語義;
從單個斷言到比對器組合,再到任意擴充的流式斷言;
對領域模型的封裝逐漸做到強大,以及對java新技術棧的支援。
總結一句:推薦使用強大的AssertJ作為你的斷言工具。
這裡以jmockit(1.31版本)為例作說明,目的是想讓大家在使用mock之前了解一下mock的過程,以及Instrumentation基于JVM的動态代理技術 & ASM位元組碼技術。知其然亦知其是以然。
另外, 其他Mock工具的動态代理的思路是一緻的,但是具體技術不同,例如EasyMock、jMock、Mockito等對于接口的mock是基于java.lang.reflect.Proxy技術,生成一個新的實作類并在運作時AOP替換;或者對于非接口的非final類使用CGLIB技術動态生成子類。不過這樣的技術處理會導緻final類、構造方法以及一些不能被覆寫的方法不能被mock,有興趣的同學可以挑一個架構研究一下源碼。
Instrumentation。
解決問題: Instrumentation實作了JVM運作時對類進行動态控制和解釋的這樣一個動态代理。
架構初始化。Jmockit實作了自己的junit Runner,在Test架構初始化的時候,會一并初始化自己的動态代理,即Instrumentation的運作環境和初始資料。
期望錄制。主要是對聲明為MockUp對象的類和方法轉化為位元組碼數組,通過Instrumentation對記憶體方法進行重新定義,并注冊到本地的環境變量。
替換預期值。執行測試方法,通過Instrumentation的動态代理監聽MockUp類調用,并替換錄制結果,實作虛拟機級别的AOP功能。
接口(&dollar;Impl_)、抽象類(&dollar;Subclass_)、普通類三種mock方式
以接口mock舉例:根據mockup類建立mock的實作類;并且将mock類和執行個體注冊到MockClasses的環境變量中;用目前mock類的位元組碼數組通過instrumentation改變被mock的類和方法。
instrumentation的運作時jvm通訊接口
基于上面的比較,能看出JMockit功能是強大的,是以做推薦。另外從源碼裡能看到JMockit對Junit4/5,spring-test,以及testNG等架構都分别實作裝飾器,做了較好的适配;Instrumentation Attach的虛拟機支援:Bsd/HotSpot/Linut/Windows/Solaris等;此外Jmockit自帶覆寫率(coverage)統計能力,也是其他mock架構不具有的。下面對Jmockit的api和常用的寫法做一些示例說明。
JMockit有兩種mock方式:
Behavior-oriented(Expectations & Verifications) :對mock目标代碼的行為進行模仿,更像黑盒測試。
State-oriented(MockUp): 基于狀态的mock。可以對傳入的參數進行檢查、比對,才傳回某些結果,類似白盒;基本上可以mock任何代碼或邏輯。
@Mocked:被修飾的對象将會被Mock,對應的類和執行個體都會受影響
@Injectable:僅Mock被修飾的執行個體
@Capturing:可以mock接口以及其所有的實作類
@Mock:MockUp模式中,指定被Fake的方法
Expectations:期望,指定的方法必須被調用
StrictExpectations:嚴格的期望,指定方法必須按照順序調用
Verifications:驗證
VerificationsInOrder:有順序的驗證
Invocation:工具類,可以擷取調用資訊
Delegate:自己指定傳回值,适合那種需要參數決定傳回值的場景,隻需指定匿名子類就可以。
MockUp:模拟函數實作
Deencapsulation:反射工具類
這裡說明一下,不僅public方法, private、protected 或者包保護級别的方法,以及static、final、native本地方法都是可以像例子裡一樣mock處理的。
用途:有些諸如servicelocator等類在初始化時做一些上下文加載的事情,如果不想運作相關初始化邏輯,即可用$clinit()模拟掉。
每個被mock的方法包括構造方法,都可以選擇性的在第一個參數的位置添加Invocation參數。作用很多,例如擷取目前mock執行個體,或者目前mock方法被執行的次數;
Deencapsulation工具能使用反射技術,操作類外部不可見屬性。這為那些專門設計的,字段在外部不可操控的類的測試提供了友善。
mock方法中可以通過調用mock執行個體的proceed()方法,來執行原來的真實的方法
<a href="http://gitlab.alibaba-inc.com/afzet/UT/">http://gitlab.alibaba-inc.com/afzet/UT/</a>