OC單元測(cè)試

轉(zhuǎn)載自你應(yīng)該知道的單元測(cè)試

一、單元測(cè)試

單元測(cè)試,一個(gè)不斷被強(qiáng)調(diào),又不斷被人忽略的話題,想從屌絲程序員晉級(jí)成高級(jí)工程師,單元測(cè)試,可以說(shuō)是必不可少的技能。如何編寫合適的測(cè)試用例?何時(shí)該進(jìn)行單元測(cè)試?單元測(cè)試所體現(xiàn)的價(jià)值究竟是什么?可以說(shuō),有很多實(shí)際的困擾阻礙著一批人,使得這些人被卡在了單元測(cè)試的門外,萬(wàn)事起步難,而當(dāng)你真正的理解了一件事情的意圖,就能很容易的從各個(gè)方面入手了。

二、單元測(cè)試的價(jià)值

減少低級(jí)錯(cuò)誤

這一點(diǎn)是毋庸質(zhì)疑的,測(cè)試所存在的最主要價(jià)值就是幫我們解決錯(cuò)誤,單元測(cè)試也是這樣。當(dāng)我們?cè)趯?duì)自己的代碼進(jìn)行測(cè)試時(shí),能很容易的就排除掉一些非常低級(jí)的錯(cuò)誤,起碼我們能夠保證,在一些正常的情況下,代碼是可以正常工作的。

當(dāng)經(jīng)歷的語(yǔ)言和平臺(tái)越來(lái)越多,很多平臺(tái)相關(guān)的特性有時(shí)候并不是靠感覺(jué)就能拿得準(zhǔn)的,比如你并不清楚NSString對(duì)象的equalTo和equalToString這兩個(gè)方法執(zhí)行效果是否相同,那么你就有必要對(duì)使用到的代碼進(jìn)行測(cè)試去驗(yàn)證下,避免出現(xiàn)人為意識(shí)造成的低級(jí)Bug。

減少調(diào)試時(shí)間

可以說(shuō),在開(kāi)發(fā)中我們有大部分的時(shí)間可能都是出于調(diào)試狀態(tài),減少調(diào)試時(shí)間,自然也就提高了產(chǎn)出率,而單元測(cè)試是否能提高產(chǎn)出率一直也是有點(diǎn)爭(zhēng)議,不過(guò)它的確能夠有效的減少調(diào)試時(shí)間。

在一個(gè)應(yīng)用中,并不是所有需要調(diào)試的代碼都在程序的入口點(diǎn),所以,當(dāng)我們需要調(diào)試時(shí),會(huì)花費(fèi)一些額外的時(shí)間來(lái)觸發(fā)調(diào)試的代碼。單元測(cè)試就能很好的解決這個(gè)問(wèn)題,我們針對(duì)需要調(diào)試的代碼,構(gòu)建相關(guān)測(cè)試上下文,配合IDE,能方便快速的進(jìn)行反復(fù)模擬、測(cè)試。

描述代碼行為

很多書(shū)上都會(huì)說(shuō),代碼就是最好的文檔(當(dāng)然是寫得比較好的代碼),注釋需要能夠精簡(jiǎn),否則大片的注釋會(huì)影響閱讀。這點(diǎn)我是非常贊同的,而單元測(cè)試,作為代碼的一等公民,我覺(jué)得它能更好的描述代碼的行為。在撰寫單元測(cè)試時(shí),我們基本上都是假定某個(gè)方法,在某個(gè)特定的環(huán)境中,能夠有預(yù)期的表現(xiàn)。如果這樣的測(cè)試足夠完善,那么,當(dāng)我們?nèi)タ磩e人測(cè)試時(shí),就能很清楚他提供的方法是為了適應(yīng)怎樣的場(chǎng)景,能夠更好的理解設(shè)計(jì)者的意圖。

可維護(hù)性增強(qiáng)

當(dāng)一個(gè)項(xiàng)目中單元測(cè)試的覆蓋率很可觀,后期在對(duì)代碼進(jìn)行修改時(shí),能夠很容易就知道是否破壞了老的業(yè)務(wù)邏輯,這樣大大的降低了回歸出錯(cuò)的可能性。當(dāng)我們從測(cè)試那獲得一個(gè)Bug時(shí),可以通過(guò)測(cè)試用例去還原,當(dāng)我們這個(gè)測(cè)試通過(guò)后,這個(gè)Bug也就解決了,而這個(gè)Bug Fix的測(cè)試用例也保證了以后這個(gè)Bug不會(huì)再次復(fù)現(xiàn)。

這會(huì)是一個(gè)很好的良性循環(huán),我們的代碼會(huì)越來(lái)越健壯,而我們可以把心思放在更多更有意義的事情上,比如重構(gòu)。有了單元測(cè)試的保障,我們可以比較大膽的進(jìn)行重構(gòu)設(shè)計(jì),當(dāng)然,在重構(gòu)時(shí)單元測(cè)試也會(huì)成為一種負(fù)擔(dān),我們可能需要同時(shí)重構(gòu)單元測(cè)試,不過(guò),相比于可靠性,這種負(fù)擔(dān)還是非常值得去承受的。

改善設(shè)計(jì)

測(cè)試驅(qū)動(dòng)設(shè)計(jì),這在敏捷開(kāi)發(fā)中是非?;馃岬拿~,但我自身并不認(rèn)為在一個(gè)較大型的項(xiàng)目中,能夠完全按照這樣的方式來(lái)驅(qū)動(dòng)。雖然如此,但測(cè)試從一定的程度上能夠改善設(shè)計(jì),比如為了讓一些類的某些行為中的細(xì)節(jié)得到充分測(cè)試(心里不再惴惴不安),我們就必須要對(duì)這些行為進(jìn)行細(xì)分,于是我們開(kāi)始提取方法,構(gòu)建測(cè)試用例。這樣,我們方法的行為會(huì)越來(lái)越單一,而良好的類設(shè)計(jì)中,正是需要這樣的方法設(shè)計(jì)。

三、測(cè)試用例三部曲

如何比較好的來(lái)編寫一個(gè)測(cè)試用例,對(duì)此,有很多不同的做法,而這也并沒(méi)有一個(gè)標(biāo)準(zhǔn),也不需要有一個(gè)標(biāo)準(zhǔn)。我們需要清楚一個(gè)測(cè)試用例存在的意義是什么,它是為了驗(yàn)證某個(gè)類的某個(gè)行為在某種上下文中能得到預(yù)期的結(jié)果,如果你的測(cè)試用例達(dá)到了這樣的目的,那么如何寫也都不算錯(cuò)。不過(guò),為了能夠統(tǒng)一單元測(cè)試的規(guī)范(這點(diǎn)在多人協(xié)同開(kāi)發(fā)下非常重要),我們常常會(huì)把一個(gè)測(cè)試用例分為三個(gè)階段:排列資源、執(zhí)行行為、斷言結(jié)果,一般我會(huì)習(xí)慣用Arrange、Act、Assert來(lái)表示,也會(huì)有用Given,When,Then來(lái)表示的,但意思都相同。

排列資源

排列資源,便是提供一切測(cè)試方法所需要的東西,而這些東西便稱之為資源。這些資源包括:

  • 1.方法輸入的參數(shù)
  • 2.方法所執(zhí)行的特定上下文

這個(gè)階段相當(dāng)于準(zhǔn)備階段,一切都是為了這個(gè)用例中執(zhí)行行為而準(zhǔn)備,如果沒(méi)有任何需要準(zhǔn)備的數(shù)據(jù),這個(gè)階段是可以被忽略的。

這里我們以測(cè)試 NSMutableDictionarydic setObject: forKey: 為例,那么在排列資源階段,我們的代碼如下:

- (void)test_setObject$forKey {
    // arrange
    NSString *key = @"test_key";
    NSString *value = @"test_value";
    NSMutableDictionary *dic = [NSMutableDictionary new];
}

關(guān)于測(cè)試用例的命名,比較推崇這樣的寫法:

test_測(cè)試方法簽名_測(cè)試上下文

由于Objective-C的方法簽名比較奇怪,為了可讀性,建議使用$進(jìn)行分割,比如這個(gè)示例中的test_setObject$forKey,或者附帶上下文的test_setObject$forKey_when_key_is_nil。

執(zhí)行行為

當(dāng)準(zhǔn)備階段完畢后,便進(jìn)入要測(cè)試行為的執(zhí)行階段,在這個(gè)階段,我們會(huì)使用準(zhǔn)備好的資源,并記錄下行為輸出以供下個(gè)階段使用。這里的行為輸出不一定就是方法執(zhí)行的返回值,很多時(shí)候我們要測(cè)試的方法并沒(méi)有任何返回值,但一個(gè)方法執(zhí)行后,總歸有一個(gè)預(yù)期的行為發(fā)生,即便是空方法也是(什么也不會(huì)改變),而這個(gè)行為預(yù)期便是測(cè)試行為的輸出。

加入執(zhí)行行為的代碼:

- (void)test_setObject$forKey {
    // arrange
    NSString *key = @"test_key";
    NSString *value = @"test_value";
    NSMutableDictionary *dic = [NSMutableDictionary new];

    // act
    [dic setObject:value forKey:key];
}
斷言結(jié)果

最后一步,也是核心的一步,它決定著一個(gè) 測(cè)試用例的成功與否,我們需要在這一步斷言執(zhí)行行為的輸出是否達(dá)到預(yù)期。確定一個(gè)行為的輸出,我們可能需要多次斷言,這里需要遵循一個(gè)原則:** 先執(zhí)行的斷言,不應(yīng)該以以后斷言的成功為前提。** 以上原則很重要,這對(duì)快速排除Bug會(huì)很有幫助?,F(xiàn)在,我們來(lái)看下針對(duì) NSMutableDictionary 的這個(gè)完整測(cè)試用例:


- (void)test_setObject$forKey {
    // arrange
    NSString *key = @"test_key";
    NSString *value = @"test_value";
    NSMutableDictionary *dic = [NSMutableDictionary new];

    // act
    [dic setObject:value forKey:key];

    // assert
    XCTAssertNotNil([dic objectForKey:key]);
    XCTAssertEqual([dic objectForKey:key], value);
}

可以看到,最后我們先斷言是否為空,再斷言是否相等,后者是在前者成功的前提下才可能不失敗。如果顛倒順序,就很難盡早的發(fā)現(xiàn)錯(cuò)誤原因,我們應(yīng)該下意識(shí)的將這種斷言的依賴關(guān)系排序正確,就像我們?cè)诤芏嗾Z(yǔ)言里使用 try...catch 時(shí),我們會(huì)排列好異常捕獲的順序。

四、做到真正的單元測(cè)試

不知道大家有沒(méi)有認(rèn)真想過(guò),這種測(cè)試為什么要叫Unit Test?顧名思義,是針對(duì)Unit來(lái)進(jìn)行測(cè)試,也就是針對(duì)基本的單元進(jìn)行測(cè)試。所以要做到真正的單元測(cè)試,你需要保證你每個(gè)測(cè)試用例所針對(duì)的僅僅是一個(gè)基本的單元,而不是很多復(fù)雜依賴的綜合行為。

關(guān)于行為測(cè)試

在面向?qū)ο蟮某绦蛟O(shè)計(jì)中,一般最基本的單元就是一個(gè)類的方法,所以在單元測(cè)試中,我們要面對(duì)的就是針對(duì)這些方法編寫合適的測(cè)試用例。方法就是一個(gè)類的對(duì)外行為,針對(duì)方法的測(cè)試也可以看做是針對(duì)一個(gè)類的行為測(cè)試,在編寫測(cè)試用例時(shí),我們不應(yīng)該考慮一個(gè)行為的中間產(chǎn)出,我們應(yīng)該將關(guān)注點(diǎn)放在最終的測(cè)試執(zhí)行結(jié)果上。

關(guān)于行為測(cè)試,目前已經(jīng)有一套相關(guān)的理論和相應(yīng)的測(cè)試框架,可以參考 行為驅(qū)動(dòng)開(kāi)發(fā)

關(guān)于隔離依賴

前面也提到了,我們要的是針對(duì)一個(gè)基本單元的測(cè)試,這樣的要求會(huì)促使我們改善設(shè)計(jì)。我們應(yīng)該竟可能讓類方法的職責(zé)單一,這會(huì)方便我們撰寫測(cè)試用例。理想中,每個(gè)類都是獨(dú)立的,但現(xiàn)實(shí)里,一個(gè)類很少會(huì)沒(méi)有依賴關(guān)系,而在編寫測(cè)試用例時(shí),我們不應(yīng)該將依賴的類行為納入到該類的測(cè)試用例中,被依賴的類應(yīng)該是經(jīng)過(guò)了單獨(dú)測(cè)試,我們需要假定它是完全合理正確的。

為了能夠不受依賴類的實(shí)例影響,我們可以將依賴的行為抽象成接口,依賴類去實(shí)現(xiàn)這樣一個(gè)接口,最終可以通過(guò)構(gòu)造函數(shù)或者其他方式注入進(jìn)來(lái)。我們通過(guò)單元測(cè)試,又將設(shè)計(jì)推導(dǎo)到了另一個(gè)高度:依賴抽象而不是實(shí)現(xiàn)具體細(xì)節(jié)。 通過(guò)接口隔離依賴后,在單元測(cè)試?yán)?,我們可以撰寫一些用于測(cè)試的模擬實(shí)現(xiàn),也就是我們實(shí)現(xiàn)這樣一個(gè)接口,但只是為了測(cè)試某種行為去實(shí)現(xiàn)它,這便是所謂的 ** Mock**。

手動(dòng)實(shí)現(xiàn)一個(gè)個(gè)Mock是非常耗時(shí)的,為了測(cè)試不同的行為,我們可能需要不同的Mock對(duì)象,幸好幾乎每一平臺(tái)的單元測(cè)試都會(huì)有相應(yīng)得Mock框架,Objective-C也不例外,推薦使用 OCMockito ,官方示例也很有代表性:

// mock creation
NSMutableArray *mockArray = mock([NSMutableArray class]);

// using mock object
[mockArray addObject:@"one"];
[mockArray removeAllObjects];

// verification
[verify(mockArray) addObject:@"one"];
[verify(mockArray) removeAllObjects];

雖然這個(gè)Mock框架可以構(gòu)建Class級(jí)別的模擬抽象,但,我們應(yīng)該把這種Class當(dāng)做是其它語(yǔ)義平臺(tái)中的抽象類。前面說(shuō)過(guò)了,我們應(yīng)該盡可能的依賴于抽象,而不是實(shí)現(xiàn)細(xì)節(jié)。

五、接口模擬與集成測(cè)試

為什么我們需要通過(guò)模擬去測(cè)試類的行為?既然這個(gè)類有依賴,何不將他依賴的具體實(shí)現(xiàn)直接使用在測(cè)試用例里?這樣單元測(cè)試和運(yùn)行時(shí)效果還會(huì)更加接近。

相信很多人都有過(guò)上面這樣的疑問(wèn),其實(shí)根本原因還是很簡(jiǎn)單的:關(guān)注點(diǎn)更單一。怎樣才能做好一件事情,那就要足夠的專注,任何所謂的成功都離不開(kāi)專注。單元測(cè)試專注于一個(gè)單元的測(cè)試,它不是多個(gè)單元糅合在一起,這樣才能保證變化點(diǎn)都集中在被測(cè)試的單元中,才能體現(xiàn)出更好的維護(hù)價(jià)值。
那么,當(dāng)我們幾乎將所有類的公開(kāi)行為都進(jìn)行了單元測(cè)試,這時(shí)候我們就應(yīng)該去編寫集成測(cè)試了,集成測(cè)試與單元測(cè)試的關(guān)注點(diǎn)不同,它關(guān)心的是實(shí)現(xiàn)類在特定場(chǎng)景下交互的最終結(jié)果,可以說(shuō)集成測(cè)試會(huì)更加動(dòng)態(tài),它可以模擬很多業(yè)務(wù)場(chǎng)景,而單元測(cè)試相對(duì)比較靜態(tài),它只是用來(lái)驗(yàn)證一個(gè)動(dòng)作的正確性。

所以,在優(yōu)良的測(cè)試項(xiàng)目中,單元測(cè)試會(huì)和集成測(cè)試分開(kāi),當(dāng)然現(xiàn)實(shí)中不一定會(huì)這么做。就比如我們測(cè)試 REST API 時(shí),單元測(cè)試應(yīng)該回去模擬網(wǎng)絡(luò)返回的數(shù)據(jù),而集成測(cè)試才會(huì)真實(shí)的發(fā)送網(wǎng)絡(luò)請(qǐng)求,很多時(shí)候我們都直接使用了后者,這樣做感覺(jué)很方便,而好壞就留給大家自己去斟酌吧。

六、總而言之

經(jīng)過(guò)漫長(zhǎng)的歲月洗禮,你終會(huì)變成一個(gè)熱愛(ài)它的猿。單元測(cè)試的利弊需要你在不同的項(xiàng)目中反復(fù)斟酌,任何一門技術(shù)都是需要不斷總結(jié),從而能向更高層次演化。從現(xiàn)在開(kāi)始,讓單元測(cè)試來(lái)幫你描述代碼行為,并保證它的健壯性,而不是人為去規(guī)避一些設(shè)計(jì)缺陷。

本章并沒(méi)有提供很多實(shí)際場(chǎng)景的測(cè)試方式,但理解了這件事情的動(dòng)機(jī)后,便可以自己去處理各種細(xì)枝末節(jié)。任何測(cè)試方式,它們的中心思想也是萬(wàn)變不離其宗,只是手段不同罷了。授人以魚(yú)不如授人以漁,有了良好的基礎(chǔ)思想,我相信通過(guò)強(qiáng)大的搜索引擎,我們一定也可以在這個(gè)領(lǐng)域里找到一份屬于自己的歸屬感。

單元測(cè)試時(shí)可能遇到的坑:找不到文件

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,534評(píng)論 19 139
  • 摘自http://www.51testing.com/html/75/n-3721875.html 單元測(cè)試,一個(gè)...
    許小小晴閱讀 497評(píng)論 0 1
  • 文章來(lái)自:http://blog.csdn.net/mj813/article/details/52451355 ...
    好大一只鵬閱讀 9,346評(píng)論 2 126
  • 單元測(cè)試不是一個(gè)小工程,需要多用些時(shí)間才能做好,不要希望通過(guò)這個(gè)文章就能掌握單元測(cè)試,這只是一個(gè)入門,需要自己動(dòng)手...
    勇不言棄92閱讀 8,091評(píng)論 9 60
  • Android單元測(cè)試介紹 處于高速迭代開(kāi)發(fā)中的Android項(xiàng)目往往需要除黑盒測(cè)試外更加可靠的質(zhì)量保障,這正是單...
    東經(jīng)315度閱讀 3,418評(píng)論 6 37

友情鏈接更多精彩內(nèi)容