工作多年后我更體會(huì)到單元測試的重要性
對于有經(jīng)驗(yàn)的開發(fā)寫單元測試是非常有必要的,并且對自己的代碼質(zhì)量以及編碼能力也是有提高的。單元測試可以幫助減少bug泄露,通過運(yùn)行單元測試可以直接測試各個(gè)功能的正確性,bug可以提前發(fā)現(xiàn)并解決,由于可以跟斷點(diǎn),所以能夠比較快的定位問題,比泄露到生產(chǎn)環(huán)境再定位要代價(jià)小很多。同時(shí)充足的UT是保證重構(gòu)正確性的有效手段,有了足夠的UT防護(hù),才能放開手腳大膽重構(gòu)已有代碼,工 作多年后更了解了UT,了解了UT的重要性。
單元測試
在敏捷的開發(fā)理念中,覆蓋全面的自動(dòng)化測試是添加新特性和重構(gòu)的必要前提。單元測試在軟件開發(fā)過程中的重要性不言而喻,特別是在測試驅(qū)動(dòng)開發(fā)的開發(fā)模式越來越流行的前提下,單元測試更成為了軟件開發(fā)過程中不可或缺的部分。同時(shí)單元測試也是提高軟件質(zhì)量,花費(fèi)成本比較低的重要方法。
1.單元測試的時(shí)機(jī)和測試點(diǎn)
1.1單元測試的時(shí)機(jī)
在業(yè)務(wù)代碼前編寫單元測試采用測試驅(qū)動(dòng)開發(fā),這是我們經(jīng)常使用和推薦的。 在業(yè)務(wù)代碼過程中進(jìn)行單元測試,對重要的業(yè)務(wù)邏輯和復(fù)雜的業(yè)務(wù)邏輯進(jìn)行添加測試。 在業(yè)務(wù)邏輯之后再編寫測試是我們不建議的,除非對遺留代碼的修改,需要先進(jìn)行測試用例的添加,保證我們修改和重構(gòu)后的代碼不會(huì)破壞之前的業(yè)務(wù)邏輯。
1.2單元測試的測試點(diǎn)
在邏輯復(fù)雜的代碼中添加測試。 在容易出錯(cuò)的地方添加測試。 不易理解的代碼中添加測試,在以后看到測試就可以非常清楚代碼要實(shí)現(xiàn)的邏輯。 在考慮后期需求變更相對較大的代碼中添加測試,這樣后期需求更變修改代碼之后就不用太擔(dān)心寫的代碼對不對以及是否破壞了已有代碼邏輯。 外部接口處添加解耦代碼、同時(shí)增加單元測試。
2.代碼不可測試性的根源
代碼中調(diào)用到了底層平臺(tái)的接口或只有系統(tǒng)運(yùn)行后才能獲得的資源(數(shù)據(jù)庫連接、發(fā)送郵件,網(wǎng)絡(luò)通訊,遠(yuǎn)程服務(wù), 文件系統(tǒng)等)但業(yè)務(wù)代碼與這些資源未解耦。這樣在測試代碼需要?jiǎng)?chuàng)建這個(gè)類的時(shí)候會(huì)去初始化這些資源時(shí)導(dǎo)致無法測試。 在方法內(nèi)部new一個(gè)與本次測試無關(guān)的對象。 代碼依賴層次很深,邏輯復(fù)雜,一次方法的往往要調(diào)用N次底層的接口,或者類的方法非常多。這樣的代碼我們需要對類進(jìn)行重構(gòu),盡量保證類的單一職責(zé):這個(gè)類在系統(tǒng)中的意圖應(yīng)當(dāng)是單一的,且修改它的原因應(yīng)該只有一個(gè)。 使用單例類和靜態(tài)方法,并且單例類和靜態(tài)方法使用到了我們底層的接口或者其他接口。
3.測試工具使用和測試方法介紹
在做單元測試的時(shí)候,我們會(huì)發(fā)現(xiàn)我們要測試的方法會(huì)引用很多外部依賴的對象,如調(diào)用平臺(tái)接口、連接數(shù)據(jù)庫、網(wǎng)絡(luò)通訊、遠(yuǎn)程服務(wù)、FTP、文件系統(tǒng)等等。 而我們沒法控制這些外部依賴的對象,為了解決這個(gè)問題,我們就需要用到Mock工具來模擬這些外部依賴的對象,來完成單元測試。 現(xiàn)在比較流行的Mock工具有JMock、EasyMock、Mockito、PowerMock。我們使用的是Mockito和PowerMock。PowerMock彌補(bǔ)了其他3個(gè)Mock工具不能mock靜態(tài)、final 、私有方法的缺點(diǎn)。 在下面的情況下我們可以使用Mock對象來完成單元測試。
實(shí)對象具有不可確定的行為,會(huì)產(chǎn)生不可預(yù)測的結(jié)果。 如:數(shù)據(jù)庫查詢可以查出一條記錄、多條記錄、或者返回?cái)?shù)據(jù)庫異常等結(jié)果。 真實(shí)對象很難被創(chuàng)建。如:平臺(tái)代碼,或者Web、JBoss容器等。 真實(shí)對象的某些行為很難觸發(fā)。 如:代碼中需要處理的網(wǎng)絡(luò)異常、數(shù)據(jù)庫異常、消息發(fā)送異常等。 真實(shí)情況令程序運(yùn)行很慢。 在敏捷的實(shí)踐中我們完成了CI,在開發(fā)提交代碼前需要執(zhí)行整個(gè)項(xiàng)目的單元測試用例,只有測試通過才可以提交代碼。這就要求我們每個(gè)單元測試用例需要盡可能的短,整個(gè)項(xiàng)目的測試時(shí)間才會(huì)短。當(dāng)有的測試用例需要測試大數(shù)據(jù)量情況下系統(tǒng)的預(yù)期時(shí),就需要使用Mock對象。 如我們代碼中需要判斷只有當(dāng)系統(tǒng)的緩存隊(duì)列大于40000時(shí),我們開始考慮丟棄非關(guān)鍵的消息,當(dāng)超過48000時(shí),需要只處理最重要的消息,當(dāng)超過50000時(shí)需要丟棄全部消息。此時(shí)就需要對此緩存隊(duì)列進(jìn)行Mock,根據(jù)調(diào)用返回不同的數(shù)據(jù)量給測試。 測試需要知道真實(shí)對象是如何被調(diào)用的。如:測試用例需要驗(yàn)證是否發(fā)送了JMS,此時(shí)就可以通過Mock對象是否被調(diào)用來測試。 真實(shí)對象實(shí)際不存在時(shí)。 如:當(dāng)我們與其他模塊交互時(shí),或者與新的接口打交道時(shí),更有就是對方的代碼還沒有開發(fā)完畢時(shí),我們可以通過Mock來模擬接口的行為,實(shí)現(xiàn)代碼邏輯的驗(yàn)證和測試。
3.1 Mocktio簡單使用說明
mock可以模擬各種各樣的對象,從而代替真正的對象做出希望的響應(yīng)。
1、模擬對象的創(chuàng)建
List cache = mock(ArrayList.class);
System.out.println(cache.get(0));
//-> null 由于沒有對mock對象給預(yù)期,所以返回都是null
2、模擬對象方法調(diào)用的返回值
List cache = mock(ArrayList.class);
when(cache.get(0)).thenReturn("hello");
System.out.println(cache.get(0));
//-> hello
3、模擬對象方法多次調(diào)用和多次返回值
List cache = mock(ArrayList.class);
when(cache.get(0)).thenReturn("0").thenReturn("1").thenReturn("2");
System.out.println(cache.get(0));
System.out.println(cache.get(0));
System.out.println(cache.get(0));
System.out.println(cache.get(0));
//-> 0,1,2,2 如果實(shí)際調(diào)用的次數(shù)超過了預(yù)期的次數(shù),則會(huì)一直返回最后一次的預(yù)期值。
4、模擬對象方法調(diào)用拋出異常
List cache = mock(ArrayList.class);
when(cache.get(0)).thenReturn(new Exception("Exception"));
System.out.println(cache.get(0));
5、模擬對象方法在沒有返回值時(shí)也可以拋異常
List cache = mock(ArrayList.class);
doThrow(new Exception("Exception")).when(cache).clear();
6、模擬方法調(diào)用時(shí)的參數(shù)匹配
AnyInt的使用,匹配任何int參數(shù)
List cache = mock(ArrayList.class);
when(cache.get(anyInt())).thenReturn("0");
System.out.println(cache.get(0));
System.out.println(cache.get(2));
//-> 0,0
7、模擬方法是否被調(diào)用和調(diào)用的次數(shù),預(yù)期調(diào)用了一次
List cache = mock(ArrayList.class);
cache.add("steven");
verify(cache).add("steven");
預(yù)期調(diào)用了兩次入緩存,沒有調(diào)用清除緩存的方法
List cache = mock(ArrayList.class);
cache.add("steven");
cache.add("steven");
verify(cache,times(2)).add("steven");
verify(cache,never()).clear();
還可以通過atLeast(int i)和atMost(int i)來替代times(int i)來驗(yàn)證被調(diào)用的次數(shù)最小值和最大值。【注意】Mock對象默認(rèn)情況下,對于所有有返回值且沒有預(yù)期過的方法,Mocktio會(huì)返回相應(yīng)的默認(rèn)值。
對于內(nèi)置類型會(huì)返回默認(rèn)值,如int會(huì)返回0,布爾值返回false。對于其他type會(huì)返回null。mock對象會(huì)覆蓋整個(gè)被mock的對象,因此沒有預(yù)期的方法只能返回默認(rèn)值。這個(gè)在初次使用Mock時(shí)需要注意,經(jīng)常會(huì)發(fā)現(xiàn)測試結(jié)果不對,最后才發(fā)現(xiàn)自己未給相應(yīng)的預(yù)期。
3.2 PowerMock簡單使用說明
PowerMock使用一個(gè)自定義類加載器和字節(jié)碼操作來模擬靜態(tài)方法,構(gòu)造函數(shù),final類和方法,私有方法,去除靜態(tài)初始化器等等。 PowerMock使用簡單,在類名前添加注解,在預(yù)期前調(diào)用PowerMock的mock靜態(tài)類方法,其他的預(yù)期方法和Mockito類似。
@PrepareForTest(System.class)
@RunWith(PowerMockRunner.class)
public class Test {
@org.junit.Test
public void should_get_filed() {
System.out.println(System.getProperty("myName"));
PowerMockito.mockStatic(System.class);
PowerMockito.when(System.getProperty("myName")).thenReturn("steven");
System.out.println(System.getProperty("myName"));
//->null steven
}
}
3.3 Fake對象的使用
測試中需要模擬對象,除了常用的mock對象外,我們還會(huì)經(jīng)常用到Fake對象。Mock對象是預(yù)先計(jì)劃好的對象,帶有各種期待,他們組成了一個(gè)關(guān)于他們期待接受的調(diào)用的詳細(xì)說明。
而Fake對象是有實(shí)際可工作的實(shí)現(xiàn),但是通常有一些缺點(diǎn)導(dǎo)致不適合用于產(chǎn)品,我們通常使用Fake對象在測試中來模擬真實(shí)的對象。 在測試中經(jīng)常會(huì)發(fā)現(xiàn)我們需要使用系統(tǒng)或者平臺(tái)給我們提供的接口,在測試中我們可以新創(chuàng)建一個(gè)類去實(shí)現(xiàn)此接口,然后在根據(jù)具體情況去實(shí)習(xí)此模擬類的相應(yīng)方法。
如我們創(chuàng)建了自己的FakeLog對象來模擬真實(shí)的日志打印,這樣我們可以在測試類中使用FakeLog來代替代碼中真實(shí)使用的Log類,可以通過FakeLog的方法和預(yù)期的結(jié)果比較來進(jìn)行測試正確性的判斷。
Fake對象和mock對象還有一個(gè)實(shí)際中使用的區(qū)別,F(xiàn)ake對象我們構(gòu)造好后,以后所有的代碼都去調(diào)用此Fake對象就可以了,不用每個(gè)類每次都要給預(yù)期。從這個(gè)角度可以看到當(dāng)一個(gè)類的方法或者預(yù)期相對不變時(shí),可以采用Fake對象,當(dāng)這個(gè)類的返回信息預(yù)期變化非常不可預(yù)期時(shí),可以采用MOCK對象。
3.4Mock服務(wù)的兩種方式
(1)直接注入:用于類之間的依賴層次較多的情況,測試整個(gè)業(yè)務(wù)流程,粒度大。
ResourceServerService service = mock(ResourcePPUServerService.class);
new Processor().process(service );
(2)重寫protected方法返回mock對象:用于類直接依賴于該服務(wù)的情況,測試行為的細(xì)節(jié),粒度小。
ResourceServerService service = mock(ResourceServerService .class);
generator = new EutranAnrDeletingItemGenerator() {
@Override
protected ResourceServerService getService() {
return service;
}
}
3.5測試異常
Throwable有兩個(gè)直接子類:Exception和Error
1、expcetd=SomeExecption.class
@Test(expected = AssertionError.class)
public void should_occur_assertion_error_when_emb_number_is_not_eutran_or_utran_anr_delete() throws Exception {
EMBObject eMBObject = new EMBObject();
new AnrDeleteProcessor().getAnrDeleteGenerator(EMBObject);
}
@Test(expected = NumberFormatException.class)
public void should_throw_number_format_exception_when_input_string_field_greater_255() {
TransactionIDConvert.convertTransIDToLong(transactionError);
}
2、try-catch-fail只能用于Exception,Error不能用此種方式
try {
method.invoke();
fail();
} catch (Exception e) {
assertTrue(e.getCause() instanceof RuntimeException);
}
3.6私有方法—采用反射來調(diào)用
@Test
public void should_throw_runtime_exception_when_check_eutran_trap_data_fail() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
when(eutranAnrAddItemGenerator.getSrvCelProcessor()).thenReturn(processor);
when(processor.validateTrapData(any(AnrItem.class), any(AnrBean.class))).thenReturn(false);
Method method = EutranAnrAddItemGenerator.class.getDeclaredMethod("check", AnrItem.class);
method.setAccessible(true);
try {
method.invoke(eutranAnrAddItemGenerator, anrAddItem);
} catch (Exception e) {
assertTrue(e.getCause() instanceof RuntimeException);
}
}
4.單元測試的格式
4.1測試類結(jié)構(gòu)
public class ExampleTest {
@BeforeClass
public static void setUp() throws Exception {
initGlobalParameter();
registerServices();
}
@Before
public void setUp() throws Exception {
initGlobalParameter();
registerServices();
}
@After
public void tearDown() throws Exception {
ServiceAccess.serviceMap.clear();
clearCache(); }
@AfterClass
public static void tearDown() throws Exception {
ServiceAccess.serviceMap.clear();
clearCache();
}
@Test
public void should_get_some_result1_when_give_some_condition1{
}
@Test
public void should_get_some_result2_when_give_some_condition2{
}
}
JUnit4是JUnit框架有史以來的最大改進(jìn),其主要目標(biāo)便是利用Java5的Annotation特性簡化測試用例的編寫。先簡單解釋一下什么是Annotation,這個(gè)單詞一般是翻譯成元數(shù)據(jù)。元數(shù)據(jù)是什么?元數(shù)據(jù)就是描述數(shù)據(jù)的數(shù)據(jù)。也就是說,這個(gè)東西在Java里面可以用來和public、static等關(guān)鍵字一樣來修飾類名、方法名、變量名。修飾的作用描述這個(gè)數(shù)據(jù)是做什么用的,差不多和public描述這個(gè)數(shù)據(jù)是公有的一樣。
@Before:每個(gè)測試方法執(zhí)行之前都要執(zhí)行一次。 @After:before對應(yīng),每個(gè)測試方法執(zhí)行之后要執(zhí)行一次。 @BeforeClass:在所有測試方法之前運(yùn)行,只運(yùn)行一次。一般在此類中申請昂貴的外部資源。父類中有@BeforeClass方法,在其子類運(yùn)行之前也會(huì)運(yùn)行。 @AfterClass:與BeforeClass對應(yīng),在所有測試結(jié)束后,釋放BeforeClass中申請的資源。 注意:@Before,@After,@BeforeClass,@AfterClass 標(biāo)示的方法一個(gè)類中只能各有一個(gè) @Test: 告訴JUnit,該方法要作為一個(gè)測試用例來運(yùn)行。
4.2測試代碼的位置
在Java中一個(gè)包可以橫跨兩個(gè)不同的目錄,所以我們的測試代碼和產(chǎn)品代碼放在同一目錄中,這樣維護(hù)起來更方便,測試代碼和產(chǎn)品代碼在同一個(gè)包中,這樣也減少了不必要的包引起,同時(shí)在測試類中使用繼承更加的方便。
4.3測試用例格式3段式
一個(gè)測試用例主體內(nèi)容一般采用三段式:given-when-then
Given:構(gòu)造測試條件;
When:執(zhí)行待測試的方法;
Then:判斷測試結(jié)果是否符合期望。 例如:
@Test
public void should_get_correct_result_when_add_two_numbers() {
int a = 1;
int b = 2;
int c = MyMath.add(a, b);
assertEquals(3, c);
}
4.4類名的命名方式
測試類的名稱以Test結(jié)尾。從目標(biāo)類的類名衍生出其單元測試類的類名。類名前加上Test后綴。 Fake(偽類)放在測試包中,使用前綴Fake。
4.5方法名的定義方式
should …do something…when…under some conditions…
例如:
should_NOT_delete_A_when_exists_B_related_with_A
should_throw_exception_when_the_parameter_is_illegal
4.6業(yè)務(wù)代碼中為測試提供的方法的注解
在業(yè)務(wù)代碼中為了測試而單獨(dú)提供的保護(hù)方法或者其他方法,我們通過@ForTest來標(biāo)注。FofTest類如下:
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE})
public @interface ForTest {
String description() default "";
}
5.代碼中涉及外部接口時(shí),如何來編寫單元測試
我們的代碼涉及的模塊非常眾多,經(jīng)常需要相互協(xié)作來完成一個(gè)功能,在此過程中經(jīng)常需要使用到外部的接口、同時(shí)也為別的模塊提供服務(wù)。
5.1數(shù)據(jù)庫
數(shù)據(jù)庫的單元測試,由于測試無法進(jìn)行數(shù)據(jù)庫的連接,故我們通過提取通用接口(DBManagerInterface)和FakeDBManager來實(shí)現(xiàn)數(shù)據(jù)庫解耦。FakeDBManager可以對真實(shí)的數(shù)據(jù)庫進(jìn)行模擬,也就是我們通過Fake一個(gè)簡單的內(nèi)存數(shù)據(jù)庫來模擬實(shí)際真實(shí)的數(shù)據(jù)庫。 DBManager是我們的真實(shí)連接數(shù)據(jù)庫的業(yè)務(wù)類。我們在測試時(shí),是可以通過注入的方式用FakeDBManager來替換DBManager。
5.2平臺(tái)接口
5.2.1 平臺(tái)接口的Mock
平臺(tái)中的MinosMmlPPUServerService、ResourcePPUServerService等服務(wù)接口,都可以通過mock來進(jìn)行測試。需要注意的是在業(yè)務(wù)代碼中需要進(jìn)行相應(yīng)的解耦,可以通過SET方法或者構(gòu)造器來注入平臺(tái)的服務(wù)類。
public class ICMEMBMessageListenerTest {
private MinosMmlPPUServerService minosMmlPPUServerService = mock(MinosMmlPPUServerService.class);
@Before
public void setUp() throws Exception {
registerServices();
icmembMessageListener = new ICMEMBMessageListener(){
};
when(minosMmlPPUServerService.getIp()).thenReturn("127.0.0.1");
when(minosMmlPPUServerService.getPort()).thenReturn("80");
when(minosMmlPPUServerService.getEmbPort()).thenReturn("8080");
}
此處需要注意如果用到靜態(tài)變量全局唯一的,需要在使用后在 tearDown中進(jìn)行清除。
5.3 文件接口的測試
我們的業(yè)務(wù)中也會(huì)出現(xiàn)與外部文件進(jìn)行讀寫的代碼。按照單元測試書寫的原則,單元測試應(yīng)該是獨(dú)立的,不依賴于外部任何文件或者資源的。好的單元測試是運(yùn)行速度快,能夠幫助我們定位問題。所以我們普通涉及到外部文件的代碼,都需要通過mock來預(yù)期其中的信息,如MOCK(I18n)文件或者properties、xml文件中的數(shù)據(jù)。 對于一些重要的文件,考慮到資源消耗不大的情況下,我們也會(huì)去為這些文件添加單元測試。需要訪問真實(shí)的文件,我們第一步就需要去獲取資源文件的具體位置。通過下面的FileService的getFileWorkDirectory我們可以獲取單元測試運(yùn)行時(shí)的根目錄。
public class FileService {
public static String getFileWorkDirectory() {
return new StringBuilder(getFileCodeRootDirectory()).append("test").toString();
}
public static String getFileCodeRootDirectory() {
String userDir = System.getProperty("user.dir");
userDir = userDir.substring(0, userDir.indexOf(File.separator + "CODE" + File.separator));
StringBuilder workFilePath = new StringBuilder(userDir);
workFilePath.append(File.separator).append("CODE").append(File.separator);
return workFilePath.toString();
}
}
我們在單元測試中可以通過傳入具體的文件名稱,可以在測試代碼中訪問真實(shí)的文件。 這種方法可以適用I18n文件,xml文件, properties文件。 我們在對I18n文件進(jìn)行測試時(shí),也可以通過Fake對象根據(jù)具體的語言來進(jìn)行國際化信息的測試。具體FakeI18nWrapper的代碼在第7章中給出可以參考。
@Before
public void setUp() throws Exception {
String i18nFilePath = FileService.getFileWorkDirectory() + "\\conf\\i18n.xml";
I18N i18N = new FakeI18nWrapper(new File(i18nFilePath), I18nLanguageType.en_US);
I18nAnrOsf.setTestingI18NInstance(i18N);
}
6.單元測試中涉及多線程、單例類、靜態(tài)類的處理
6.1多線程測試
通過單元測試,能較早地發(fā)現(xiàn) bug 并且能比不進(jìn)行單元測試更容易地修復(fù)bug。但是普通的單元測試方法(即使當(dāng)徹底地進(jìn)行了測試時(shí))在查找并行 bug 方面不是很有效。這就是為什么在實(shí)驗(yàn)室測試沒有問題,但在外場經(jīng)常出現(xiàn)各種莫名其妙的問題。
為什么單元測試經(jīng)常遺漏并行 bug?通常的說法是并行程序和Bug的問題在于它們的不確定性。但是對于單元測試目的而言,在于并行程序是非常 確定的。所以我們單元測試需要對關(guān)鍵的邏輯、涉及到并發(fā)的場景進(jìn)行多線程測試。
多線程的不確定性和單元測試的確定的預(yù)期確實(shí)是有點(diǎn)矛盾,這就需要精心的設(shè)計(jì)單元測試中的多線程用例。 Junit本身是不支持普通的多線程測試的,這是因?yàn)镴unit的底層實(shí)現(xiàn)上是用System.exit退出用例執(zhí)行的。JVM都終止了,在測試線程啟動(dòng)的其他線程自然也無法執(zhí)行。
所以要想編寫多線程Junit測試用例,就必須讓主線程等待所有子線程執(zhí)行完成后再退出。我們一般的方法是在主測試線程中增加sleep方法,這種方法優(yōu)點(diǎn)是簡單,但缺點(diǎn)是不同機(jī)器的配置不一樣,導(dǎo)致等待時(shí)間無法確定。
更為高效的多線程單元測試可以使用JAVA的CountDownLatch和第三方組件GroboUtils來實(shí)現(xiàn)。 下面通過一個(gè)簡單的例子來說明下多線程的單元測試。 測試的業(yè)務(wù)代碼如下,功能是唯一事務(wù)號(hào)的生成器。
class UniqueNoGenerator {
private static int generateCount = 0;
public static synchronized int getUniqueSerialNo() {
return generateCount++;
}
}
6.1.1 Sleep
private static Set
@Test
public void should_get_unique_no() throws InterruptedException {
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = generateThread();
}
//啟動(dòng)線程
Arrays.stream(threads).forEach(Thread::start);
Thread.sleep(100L);
assertEquals(results.size(), 100);
}
private Thread generateThread() {
return new Thread(() -> {
int uniqueSerialNo = UniqueNoGenerator.getUniqueSerialNo();
results.add(uniqueSerialNo);
});
}
通過Sleep來等待測試線程中的所有線程執(zhí)行完畢后,再進(jìn)行條件的預(yù)期。問題就是用戶無法準(zhǔn)確的預(yù)期業(yè)務(wù)代碼線程執(zhí)行的時(shí)間,不同的環(huán)境等待的時(shí)間也是不等的。由于需要添加延時(shí),同時(shí)也違背了我們單元測試執(zhí)行時(shí)間需要盡量短的原則。
6.1.2 ThreadGroup
private static Set<Integer> results = new HashSet<>();
private ThreadGroup threadGroup = new ThreadGroup("test");
@Test
public void should_get_unique_no() throws InterruptedException {
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = generateThread();
}
//啟動(dòng)線程
Arrays.stream(threads).forEach(Thread::start);
while (threadGroup.activeCount() != 0) {
Thread.sleep(1);
}
assertEquals(results.size(), 100);
}
private Thread generateThread() {
return new Thread(threadGroup, () -> {
int uniqueSerialNo = UniqueNoGenerator.getUniqueSerialNo();
results.add(uniqueSerialNo);
});
}
這個(gè)是通過ThreadGroup來實(shí)現(xiàn)多線程測試的,可以把需要測試的類放入一個(gè)線程組,同時(shí)去判斷線程組中是否還有未結(jié)束的線程。測試中需要注意把新建的線程加入到線程組中。
6.1.3 CountDownLatch
private static Set<Integer> results = new HashSet<>();
private CountDownLatch countDownLatch = new CountDownLatch(100);
@Test
public void should_get_unique_no() throws InterruptedException {
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = generateThread();
}
//啟動(dòng)線程
Arrays.stream(threads).forEach(Thread::start);
countDownLatch.await();
assertEquals(results.size(), 100);
}
private Thread generateThread() {
return new Thread(() -> {
int uniqueSerialNo = UniqueNoGenerator.getUniqueSerialNo();
results.add(uniqueSerialNo);
countDownLatch.countDown();
});
}
通過JAVA的CountDownLatch可以很方便的來判斷,測試中的線程是否已經(jīng)執(zhí)行完畢。CountDownLatch是一個(gè)同步輔助類,在完成一組正在其他線程中執(zhí)行的操作之前,它允許一個(gè)或多個(gè)線程一直等待,我們這里是讓測試主線程等待。countDown方法是當(dāng)前線程調(diào)用此方法,則計(jì)數(shù)減一。awaint方法,調(diào)用此方法會(huì)一直阻塞當(dāng)前線程,直到計(jì)時(shí)器的值為0。
6.2單例類測試
單例模式要點(diǎn):
單例類在一個(gè)容器中只有一個(gè)實(shí)例。 單例類使用靜態(tài)方法自己提供向客戶端提供實(shí)例,自己擁有自己的引用。 必須向整個(gè)容器提供自己的實(shí)例。 單例類的實(shí)現(xiàn)方式有多種方式,如懶漢式單例、餓漢式單例、登記式單例等。我們這里采用內(nèi)部類的形式來構(gòu)造單例類,實(shí)現(xiàn)的優(yōu)點(diǎn)是此種方式不需要給類或者方法添加鎖,唯一實(shí)例的生成是由JAVA的內(nèi)部類生成機(jī)制保證。 下面的例子構(gòu)造了一個(gè)單例類,同時(shí)這個(gè)單例類我們提供了一個(gè)獲取遠(yuǎn)程Cpu信息的方法。再構(gòu)造一個(gè)使用類ResourceManager.java來模擬調(diào)用此單例類,同時(shí)看下我們測試ResourceManager.java過程中遇到的問題。 單例類DBManagerTools.java:
public class DbManager {
private DbManager() {
}
public static DbManager getInstance() {
return DbManagerHolder.instance;
}
private static class DbManagerHolder {
private static DbManager instance = new DbManager();
}
public String getRemoteCpuInfo(){
FtpClient ftpClient = new FtpClient("127.0.0.1","22");
return ftpClient.getCpuInfo();
}
}
調(diào)用類 ResourceManager.java:
public class ResourceManager {
public String getBaseInfo() {
StringBuilder buffer = new StringBuilder();
buffer.append("IP=").append("127.0.0.1").append(";CPU=").append(DbManager.getInstance().getRemoteCpuInfo());
return buffer.toString();
}
}
測試類
@Test
public void should_get_cpu_info() {
String expected = "IP=127.0.0.1;CPU=Intel";
ResourceManager resourceManager = new ResourceManager();
String baseInfo = resourceManager.getBaseInfo();
assertThat(baseInfo, is(expected));
}
從上面的描述可以看到,由于業(yè)務(wù)代碼強(qiáng)關(guān)聯(lián)了一個(gè)單例類,同時(shí)這個(gè)單例類會(huì)去通過網(wǎng)絡(luò)獲取遠(yuǎn)程機(jī)器的信息。這樣我們的單元測試在運(yùn)行中就會(huì)去連接網(wǎng)絡(luò)中的服務(wù)器導(dǎo)致測試失敗。在業(yè)務(wù)類中類似這種涉及到單例類的調(diào)用經(jīng)常用到。 這種情況下我們需要修改下業(yè)務(wù)代碼使代碼可測。 第一種方法:提取方法并在測試類中復(fù)寫。
public class ResourceManager {
public String getBaseInfo() {
StringBuilder buffer = new StringBuilder();
buffer.append("IP=").append("127.0.0.1").append("CPU=").append(getRemoteCpuInfo());
return buffer.toString();
}
@ForTest
protected String getRemoteCpuInfo() {
return DbManager.getInstance().getRemoteCpuInfo();
}
}
@Test
public void should_get_cpu_info() {
String expected = "IP=127.0.0.1;CPU=Intel";
ResourceManager resourceManager = new ResourceManager(){
@Override
protected String getRemoteCpuInfo() {
return "Intel";
}
};
String baseInfo = resourceManager.getBaseInfo();
assertThat(baseInfo, is(expected));
}
第二種方法:提取單例類中的方法為接口,然后在業(yè)務(wù)代碼中通過set方法或者構(gòu)造器注入到業(yè)務(wù)代碼中。
public class DbManager implements ResourceService{
private DbManager() {
}
public static DbManager getInstance() {
return DbManagerHolder.instance;
}
private static class DbManagerHolder {
private static DbManager instance = new DbManager();
}
@Override
public String getRemoteCpuInfo(){
FtpClient ftpClient = new FtpClient("127.0.0.1","22");
return ftpClient.getCpuInfo();
}
public interface ResourceService {
String getRemoteCpuInfo();
}
public class ResourceManager {
private ResourceService resourceService = DbManager.getInstance();
public String getBaseInfo() {
StringBuilder buffer = new StringBuilder();
buffer.append("IP=").append("127.0.0.1").append("CPU=").append(resourceService.getRemoteCpuInfo());
return buffer.toString();
}
public void setResourceService(ResourceService resourceService) {
this.resourceService = resourceService;
}
}
@Test
public void should_get_cpu_info() {
String expected = "IP=127.0.0.1;CPU=Intel";
ResourceManager resourceManager = new ResourceManager();
DbManager mockDbManager = mock(DbManager.class);
resourceManager.setResourceService(mockDbManager);
when(mockDbManager.getRemoteCpuInfo()).thenReturn("Intel");
String baseInfo = resourceManager.getBaseInfo();
assertThat(baseInfo, is(expected));
}
通過上面的方法可以方便的解開業(yè)務(wù)代碼對單例的強(qiáng)依賴,有時(shí)候我們發(fā)現(xiàn)我們的業(yè)務(wù)代碼是靜態(tài)類,這個(gè)時(shí)候你會(huì)發(fā)下第一種方法是解決不了問題的,只能通過第2中方法來實(shí)現(xiàn)。 通過上面的代碼可以看到我們應(yīng)該盡量的少用單例,在必須使用單例時(shí)可以設(shè)計(jì)接口來進(jìn)行業(yè)務(wù)與單例類的解耦。
6.3靜態(tài)類測試
靜態(tài)類與單例類類似,也可以通過提取方法后通過復(fù)現(xiàn)方法來解耦,同樣也可以通過服務(wù)注入的方式來實(shí)現(xiàn)。也可以使用PowerMock來預(yù)期方法的返回。 實(shí)際應(yīng)用中如果單例類不需要維護(hù)任何狀態(tài),僅僅提供全局訪問的方法,這種情況考慮可以使用靜態(tài)類,靜態(tài)方法比單例更快,因?yàn)殪o態(tài)的綁定是在編譯期就進(jìn)行的。
同時(shí)需要注意的是不建議在靜態(tài)類中維護(hù)狀態(tài)信息,特別是在并發(fā)環(huán)境中,若無適當(dāng)?shù)耐酱胧┒薷亩嗑€程并發(fā)時(shí),會(huì)導(dǎo)致壞的競態(tài)條件。 單例與靜態(tài)主要的優(yōu)點(diǎn)是單例類比靜態(tài)類更具有面向?qū)ο蟮哪芰?,使用單例,可以通過繼承和多態(tài)擴(kuò)展基類,實(shí)現(xiàn)接口和更有能力提供不同的實(shí)現(xiàn)。 在我們開發(fā)過程中考慮到單元測試,還是需要謹(jǐn)慎的使用靜態(tài)類和單例類。
7.代碼可測性的解耦方法
在使用一些解依賴技術(shù)時(shí),我們常常會(huì)感覺到許多解依賴技術(shù)都破壞了原有的封裝性。但考慮到代碼的可測性和質(zhì)量,犧牲一些封裝性也是可以的,封裝本身也并不是最終目的,而是幫助理解代碼的。下面在介紹下常用的解依賴方法。這些解依賴方法的思想都是通用的,采用控制反轉(zhuǎn)和依賴注入的方式來進(jìn)行。
7.1盡量減少業(yè)務(wù)代碼與平臺(tái)代碼之間的耦合
軟件開發(fā)中調(diào)用平臺(tái)服務(wù)查詢資源屬性的典型代碼:
public class DataProceeor{
private static final SomePlatFormService service = ServerService.lookup(SomePlatFormService.ID);
public static CompensateData getAttributes(String name){
service.queryCompensate(name);
}
}
這種代碼在實(shí)現(xiàn)上沒有問題,但是無法進(jìn)行單元測試(不啟動(dòng)軟件)。因?yàn)榇祟惣虞d時(shí)需要獲取平臺(tái)查詢資源相關(guān)的服務(wù),業(yè)務(wù)代碼與平臺(tái)代碼存在強(qiáng)耦合性。 在不破壞原有功能的基礎(chǔ)上對這段代碼做如下改造:
1、引入實(shí)例變量和構(gòu)造器
public class DataProceeor{
private static final SomePlatformService service = ServerService.lookup(SomePlatformService.ID);
private SomePlatformService _service;
public DataProceeor(SomePlatformService service) {
_service = service;
}
public DataProceeor() {
_service = ServerService.lookup(SomePlatformService.ID);;
}
public CompensateData getAttributes(String name){
service.queryCompensate(name);
}
}
2、增加新方法
public CompensateData getSomeAttributes(String name){
_service.queryCompensate(name);
}
3、查找代碼中所有用到方法getAttributes的地方,全部替換成getSomeAttributes。
4、完成第3步后,刪除已經(jīng)無用的變量和方法。
5、重命名引入的變量和方法,使其符合命名規(guī)范。
public class DataProceeor{
private SomePlatformService service;
public DataProceeor(SomePlatformService service){
this.service = service;
}
public DataProceeor() {
service = ServerService.lookup(SomePlatformService.ID);;
}
public static CompensateData getAttributes(String name){
service.queryCompensate(name);
}
}
6、增加對新方法的測試用例
public class DataProcessorTest {
private DataProceeor dataProceeor;
private SomePlateService somePlateService;
private Map<String, String> attributes;
@Before
public void setUp() throws Exception {
attributes.put("pci", "1");
}
@Test
public void should_get_attributes() {
somePlateService = mock(SomePlateService.class);
when(somePlateService.queryAttribue()).thenReturn(attributes);
dataProceeor = new DataProceeor();
CompensateData compensateData = dataProceeor.getAttributes("pci");
assertThat(compensateData.value(), is("1"));
assertThat(compensateData.value(), is("2"));
}
}
運(yùn)行該測試用例,發(fā)現(xiàn)最后一句斷言沒有通過: 修改最后一句斷言為:assertThat(attributeValue+"", not("2")); 再次運(yùn)行測試,測試用例通過。
7.2 擴(kuò)展平臺(tái)的部分類,實(shí)現(xiàn)測試的目的
模式1中的例子查詢資源屬性時(shí)沒有設(shè)置過濾條件,事實(shí)上大多數(shù)處理都是依賴其他處理類:
public class NotificationDispatcher {
private static Logger logger = LoggerFactory.getLogger(NotificationDispatcher.class);
public void processMessage(String notificationMsg) {
NotificationMsg notification = new Gson().fromJson(notificationMsg, NotificationMsg.class);
Map<String, String> sctpInfo;
try {
sctpInfo = new NotificationParser().parse(notification.getMessage());
logger.info("Parse notification xml success: " + sctpInfo);
NotificationProcessor processor = new NotificationProcessorFactory().getProcessor(sctpInfo.get(CONFIGURATION_TYPE));
processor.process(sctpInfo);
} catch (Exception e) {
logger.warn(String.format("Deal notification failed. Exception: %s", e.getMessage()), e);
}
}
}
在本例中,查詢MOI的Filter是在getCellMoi方法內(nèi)部構(gòu)造出來的,我們可以嘗試給getCellMoi方法編寫測試用例: 測試用例沒有通過,問題出在哪里呢? Debug代碼發(fā)現(xiàn),在getCellMoi方法內(nèi)部構(gòu)造出來的Filter和我們在測試代碼中構(gòu)造的Filter并不是同一個(gè)對象。很自然地想到為Filter類編寫子類,并覆蓋其equals方法。 用自定義的Filter代替平臺(tái)的Filter:
public String getCellMoi(String cellName){
Filter filter = new SelfFilter(cellName);
return getAttributers(filter,"moi");
}
修改后測試用例運(yùn)行通過。
7.3 巧用protedted方法實(shí)現(xiàn)測試的注入
在模式2中,由于Filter是在getCellMoi內(nèi)部構(gòu)造的,并且沒有euqals方法,導(dǎo)致無法測試。還可以用別的方法對其進(jìn)行改造。代碼示例如下: 1.提取protected方法buildFilter()
public void processMessage(String notificationMsg) {
UmeNotificationMsg umeNotificationMsg = new Gson().fromJson(notificationMsg, UmeNotificationMsg.class);
Map<String, String> sctpInfo;
try {
sctpInfo = new NotificationParser().parse(umeNotificationMsg.getMessage());
logger.info("Parse notification xml success: " + sctpInfo);
NotificationProcessor processor = getNotificationProcessorFactory().getProcessor(sctpInfo.get(CONFIGURATION_TYPE));
processor.process(sctpInfo);
} catch (Exception e) {
logger.warn(String.format("Deal notification failed. Exception: %s", e.getMessage()), e);
}
}
@ForTest
protected NotificationProcessorFactory getNotificationProcessorFactory() {
return new NotificationProcessorFactory();
}
2.在測試代碼中重寫getNotificationProcessorFactory方法
@Before
public void setUp() throws Exception {
NotificationProcessorFactory notificationProcessorFactory = mock(NotificationProcessorFactory.class);
notificationDispatcher = new NotificationDispatcher(){
@Override
protected NotificationProcessorFactory getNotificationProcessorFactory() {
return notificationProcessorFactory;
}
};
}
運(yùn)行測試,可以通過。
8、總結(jié)
UT是開發(fā)人員的利器,是開發(fā)的前置保護(hù)傘,也是寫出健壯代碼的有力保證,總之一句話不會(huì)寫UT的開發(fā)不是好廚子。
