OC單元測試

轉載自你應該知道的單元測試

一、單元測試

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

二、單元測試的價值

減少低級錯誤

這一點是毋庸質疑的,測試所存在的最主要價值就是幫我們解決錯誤,單元測試也是這樣。當我們在對自己的代碼進行測試時,能很容易的就排除掉一些非常低級的錯誤,起碼我們能夠保證,在一些正常的情況下,代碼是可以正常工作的。

當經歷的語言和平臺越來越多,很多平臺相關的特性有時候并不是靠感覺就能拿得準的,比如你并不清楚NSString對象的equalTo和equalToString這兩個方法執行效果是否相同,那么你就有必要對使用到的代碼進行測試去驗證下,避免出現人為意識造成的低級Bug。

減少調試時間

可以說,在開發中我們有大部分的時間可能都是出于調試狀態,減少調試時間,自然也就提高了產出率,而單元測試是否能提高產出率一直也是有點爭議,不過它的確能夠有效的減少調試時間。

在一個應用中,并不是所有需要調試的代碼都在程序的入口點,所以,當我們需要調試時,會花費一些額外的時間來觸發調試的代碼。單元測試就能很好的解決這個問題,我們針對需要調試的代碼,構建相關測試上下文,配合IDE,能方便快速的進行反復模擬、測試。

描述代碼行為

很多書上都會說,代碼就是最好的文檔(當然是寫得比較好的代碼),注釋需要能夠精簡,否則大片的注釋會影響閱讀。這點我是非常贊同的,而單元測試,作為代碼的一等公民,我覺得它能更好的描述代碼的行為。在撰寫單元測試時,我們基本上都是假定某個方法,在某個特定的環境中,能夠有預期的表現。如果這樣的測試足夠完善,那么,當我們去看別人測試時,就能很清楚他提供的方法是為了適應怎樣的場景,能夠更好的理解設計者的意圖。

可維護性增強

當一個項目中單元測試的覆蓋率很可觀,后期在對代碼進行修改時,能夠很容易就知道是否破壞了老的業務邏輯,這樣大大的降低了回歸出錯的可能性。當我們從測試那獲得一個Bug時,可以通過測試用例去還原,當我們這個測試通過后,這個Bug也就解決了,而這個Bug Fix的測試用例也保證了以后這個Bug不會再次復現。

這會是一個很好的良性循環,我們的代碼會越來越健壯,而我們可以把心思放在更多更有意義的事情上,比如重構。有了單元測試的保障,我們可以比較大膽的進行重構設計,當然,在重構時單元測試也會成為一種負擔,我們可能需要同時重構單元測試,不過,相比于可靠性,這種負擔還是非常值得去承受的。

改善設計

測試驅動設計,這在敏捷開發中是非常火熱的名詞,但我自身并不認為在一個較大型的項目中,能夠完全按照這樣的方式來驅動。雖然如此,但測試從一定的程度上能夠改善設計,比如為了讓一些類的某些行為中的細節得到充分測試(心里不再惴惴不安),我們就必須要對這些行為進行細分,于是我們開始提取方法,構建測試用例。這樣,我們方法的行為會越來越單一,而良好的類設計中,正是需要這樣的方法設計。

三、測試用例三部曲

如何比較好的來編寫一個測試用例,對此,有很多不同的做法,而這也并沒有一個標準,也不需要有一個標準。我們需要清楚一個測試用例存在的意義是什么,它是為了驗證某個類的某個行為在某種上下文中能得到預期的結果,如果你的測試用例達到了這樣的目的,那么如何寫也都不算錯。不過,為了能夠統一單元測試的規范(這點在多人協同開發下非常重要),我們常常會把一個測試用例分為三個階段:排列資源、執行行為、斷言結果,一般我會習慣用Arrange、Act、Assert來表示,也會有用Given,When,Then來表示的,但意思都相同。

排列資源

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

  • 1.方法輸入的參數
  • 2.方法所執行的特定上下文

這個階段相當于準備階段,一切都是為了這個用例中執行行為而準備,如果沒有任何需要準備的數據,這個階段是可以被忽略的。

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

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

關于測試用例的命名,比較推崇這樣的寫法:

test_測試方法簽名_測試上下文

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

執行行為

當準備階段完畢后,便進入要測試行為的執行階段,在這個階段,我們會使用準備好的資源,并記錄下行為輸出以供下個階段使用。這里的行為輸出不一定就是方法執行的返回值,很多時候我們要測試的方法并沒有任何返回值,但一個方法執行后,總歸有一個預期的行為發生,即便是空方法也是(什么也不會改變),而這個行為預期便是測試行為的輸出。

加入執行行為的代碼:

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

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

最后一步,也是核心的一步,它決定著一個 測試用例的成功與否,我們需要在這一步斷言執行行為的輸出是否達到預期。確定一個行為的輸出,我們可能需要多次斷言,這里需要遵循一個原則:** 先執行的斷言,不應該以以后斷言的成功為前提。** 以上原則很重要,這對快速排除Bug會很有幫助。現在,我們來看下針對 NSMutableDictionary 的這個完整測試用例:


- (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);
}

可以看到,最后我們先斷言是否為空,再斷言是否相等,后者是在前者成功的前提下才可能不失敗。如果顛倒順序,就很難盡早的發現錯誤原因,我們應該下意識的將這種斷言的依賴關系排序正確,就像我們在很多語言里使用 try...catch 時,我們會排列好異常捕獲的順序。

四、做到真正的單元測試

不知道大家有沒有認真想過,這種測試為什么要叫Unit Test?顧名思義,是針對Unit來進行測試,也就是針對基本的單元進行測試。所以要做到真正的單元測試,你需要保證你每個測試用例所針對的僅僅是一個基本的單元,而不是很多復雜依賴的綜合行為。

關于行為測試

在面向對象的程序設計中,一般最基本的單元就是一個類的方法,所以在單元測試中,我們要面對的就是針對這些方法編寫合適的測試用例。方法就是一個類的對外行為,針對方法的測試也可以看做是針對一個類的行為測試,在編寫測試用例時,我們不應該考慮一個行為的中間產出,我們應該將關注點放在最終的測試執行結果上。

關于行為測試,目前已經有一套相關的理論和相應的測試框架,可以參考 行為驅動開發

關于隔離依賴

前面也提到了,我們要的是針對一個基本單元的測試,這樣的要求會促使我們改善設計。我們應該竟可能讓類方法的職責單一,這會方便我們撰寫測試用例。理想中,每個類都是獨立的,但現實里,一個類很少會沒有依賴關系,而在編寫測試用例時,我們不應該將依賴的類行為納入到該類的測試用例中,被依賴的類應該是經過了單獨測試,我們需要假定它是完全合理正確的。

為了能夠不受依賴類的實例影響,我們可以將依賴的行為抽象成接口,依賴類去實現這樣一個接口,最終可以通過構造函數或者其他方式注入進來。我們通過單元測試,又將設計推導到了另一個高度:依賴抽象而不是實現具體細節。 通過接口隔離依賴后,在單元測試里,我們可以撰寫一些用于測試的模擬實現,也就是我們實現這樣一個接口,但只是為了測試某種行為去實現它,這便是所謂的 ** Mock**。

手動實現一個個Mock是非常耗時的,為了測試不同的行為,我們可能需要不同的Mock對象,幸好幾乎每一平臺的單元測試都會有相應得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];

雖然這個Mock框架可以構建Class級別的模擬抽象,但,我們應該把這種Class當做是其它語義平臺中的抽象類。前面說過了,我們應該盡可能的依賴于抽象,而不是實現細節。

五、接口模擬與集成測試

為什么我們需要通過模擬去測試類的行為?既然這個類有依賴,何不將他依賴的具體實現直接使用在測試用例里?這樣單元測試和運行時效果還會更加接近。

相信很多人都有過上面這樣的疑問,其實根本原因還是很簡單的:關注點更單一。怎樣才能做好一件事情,那就要足夠的專注,任何所謂的成功都離不開專注。單元測試專注于一個單元的測試,它不是多個單元糅合在一起,這樣才能保證變化點都集中在被測試的單元中,才能體現出更好的維護價值。
那么,當我們幾乎將所有類的公開行為都進行了單元測試,這時候我們就應該去編寫集成測試了,集成測試與單元測試的關注點不同,它關心的是實現類在特定場景下交互的最終結果,可以說集成測試會更加動態,它可以模擬很多業務場景,而單元測試相對比較靜態,它只是用來驗證一個動作的正確性。

所以,在優良的測試項目中,單元測試會和集成測試分開,當然現實中不一定會這么做。就比如我們測試 REST API 時,單元測試應該回去模擬網絡返回的數據,而集成測試才會真實的發送網絡請求,很多時候我們都直接使用了后者,這樣做感覺很方便,而好壞就留給大家自己去斟酌吧。

六、總而言之

經過漫長的歲月洗禮,你終會變成一個熱愛它的猿。單元測試的利弊需要你在不同的項目中反復斟酌,任何一門技術都是需要不斷總結,從而能向更高層次演化。從現在開始,讓單元測試來幫你描述代碼行為,并保證它的健壯性,而不是人為去規避一些設計缺陷。

本章并沒有提供很多實際場景的測試方式,但理解了這件事情的動機后,便可以自己去處理各種細枝末節。任何測試方式,它們的中心思想也是萬變不離其宗,只是手段不同罷了。授人以魚不如授人以漁,有了良好的基礎思想,我相信通過強大的搜索引擎,我們一定也可以在這個領域里找到一份屬于自己的歸屬感。

單元測試時可能遇到的坑:找不到文件

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,937評論 18 139
  • 摘自http://www.51testing.com/html/75/n-3721875.html 單元測試,一個...
    許小小晴閱讀 420評論 0 1
  • 文章來自:http://blog.csdn.net/mj813/article/details/52451355 ...
    好大一只鵬閱讀 9,215評論 2 126
  • 單元測試不是一個小工程,需要多用些時間才能做好,不要希望通過這個文章就能掌握單元測試,這只是一個入門,需要自己動手...
    勇不言棄92閱讀 7,882評論 9 60
  • Android單元測試介紹 處于高速迭代開發中的Android項目往往需要除黑盒測試外更加可靠的質量保障,這正是單...
    東經315度閱讀 3,153評論 6 37