iOS開發-單元測試

前言

維基百科對單元測試的定義如下:

在計算機編程中,單元測試(英語:Unit Testing)又稱為模塊測試, 是針對程序模塊(軟件設計的最小單位)來進行正確性檢驗的測試工作。程序單元是應用的最小可測試部件。
在過程化編程中,一個單元就是單個程序、函數、過程等;對于面向對象編程,最小單元就是方法,包括基類(超類)、抽象類、或者派生類(子類)中的方法。

根據不同場景,單元的定義也不一樣,通常我們將C語言的單個函數或者面向對象語言的單個類視作測試的單元。在使用單元測試的過程中,我們要知道這一點:
單元測試并不是為了證明代碼的正確性,它只是一種用來幫助我們發現錯誤的手段
單元測試不是萬能藥,它確實能幫助我們找到大部分代碼邏輯上的bug,同時,為了提高測試覆蓋率,這能逼迫我們對代碼不斷進行重構,提高代碼質量等。

內置單元測試框架

在Xcode4.x中集成了測試框架OCUnit,根據測試的目的大致可以將單元測試分為這三類:

  • 性能測試:測試代碼執行花費的時間
  • 邏輯測試:測試代碼執行結果是否符合預期
  • 異步測試:測試多線程操作代碼

在我們新建項目的時候,已經默認選擇創建單元測試的框架,除了Unit Tests之外還有一個UI Tests是iOS9推出的新特性,針對UI界面的單元測試框架。在創建項目之后,會自動生成一個appName+Tests的文件夾目錄,下面存放著單元測試的文件

一個標準的測試類文件代碼如下。其中setUp會在每一個測試用例開始前調用,用來初始化相關數據;tearDown在測試用例完成后調用,可以用來釋放變量等結尾操作;testPerformanceExample中的會將方法中的block代碼耗費時長打印出來;最后的testExample用來執行我們需要的測試操作,正常情況下,我們不使用這個方法,而是創建名為test+測試目的的方法來完成我們需要的操作:

測試用例

在每個測試用例方法的左側有個菱形的標記,點擊這個標記可以單獨的運行這個測試方法。如果測試通過沒有發生任何斷言錯誤,那么這個菱形就會變成綠色勾選狀態。使用快捷鍵command+U直接依次調用所有的單元測試。另外,可以在左側的文件欄中選中單元測試欄目,然后直觀的看到所有測試的結果。同樣的點擊右側菱形位置的按鈕可以運行單個測試方法或者文件:
單元測試總覽

另外,為了保證單元測試的正確性,我們應當保證測試用例中只存在一個類或者只發生一個類變量的屬性修改。下面是我們測試中常用的宏定義

XCTAssertNotNil(a1, format…) 當a1不為nil時成立
XCTAssert(expression, format...) 當expression結果為YES成立
XCTAssertTrue(expression, format...) 當expression結果為YES成立;
XCTAssertEqualObjects(a1, a2, format...) 判斷相等,當[a1 isEqualTo: a2]返回YES的時候成立
XCTAssertEqual(a1, a2, format...) 當a1==a2返回YES時成立
XCTAssertNotEqual(a1, a2, format...) 當a1!=a2返回YES時成立

邏輯測試

筆者新建了一個用以測試的model類,該類提供了三個接口。需要注意的是,在邏輯測試的某個操作步驟前后,應該有對應的數據發生了改變,這樣才能夠方便我們進行測試:

@interface LXDTestsModel : NSObject

@property (nonatomic, readonly, copy) NSString * name;
@property (nonatomic, readonly, strong) NSNumber * age;
@property (nonatomic, readonly, assign) NSUInteger flags;

+ (instancetype)modelWithName: (NSString *)name age: (NSNumber *)age flags: (NSUInteger)flags;

- (instancetype)initWithDictionary: (NSDictionary *)dict;
- (NSDictionary *)modelToDictionary;

@end

在測試用例中,我定義了一個testModelConvert方法用來測試模型跟json之間的轉換是否正確:

- (void)testModelConvert
{
    NSString * json = @"{\"name\":\"SindriLin\",\"age\":22,\"flags\":987654321}";
    NSMutableDictionary * dict = [[NSJSONSerialization JSONObjectWithData: [json dataUsingEncoding: NSUTF8StringEncoding] options: kNilOptions error: nil] mutableCopy];

    LXDTestsModel * model = [[LXDTestsModel alloc] initWithDictionary: dict];
    XCTAssertNotNil(model);
    XCTAssertTrue([model.name isEqualToString: @"SindriLin"]);
    XCTAssertTrue([model.age isEqual: @(22)]);
    XCTAssertEqual(model.flags, 987654321);
    XCTAssertTrue([model isKindOfClass: [LXDTestsModel class]]);

    model = [LXDTestsModel modelWithName: @"Tessie" age: dict[@"age"] flags: 562525];
    XCTAssertNotNil(model);
    XCTAssertTrue([model.name isEqualToString: @"Tessie"]);
    XCTAssertTrue([model.age isEqual: dict[@"age"]]);
    XCTAssertEqual(model.flags, 562525);

    NSDictionary * modelJSON = [model modelToDictionary];
    XCTAssertTrue([modelJSON isEqual: dict] == NO);

    dict[@"name"] = @"Tessie";
    dict[@"flags"] = @(562525);
    XCTAssertTrue([modelJSON isEqual: dict]);
}

邏輯測試的目的是為了檢測在代碼執行前后發生的變化是否符合預期,因此可以說80%左右的單元測試都是邏輯測試。最開始筆者學習單元測試的時候總有一種無從下手的感覺,但是當你從無形抽象的邏輯操作找到了數據變化的規律的時候,對應的單元測試就能很快的寫出來了

性能測試

相較于上面的邏輯測試,性能測試的地位有些尷尬。在現今的開發環境下,我們已經能通過 instrument工具很好的查找到項目中的代碼耗時點,性能測試就有種棄之可惜,食之無味的感覺了。但是為了本文的完整性,還是將這個補充完畢。筆者在測試model類中添加了類方法,用來隨機生成100個類實例對象,并且在每次創建對象后讓線程休眠一段時間來模擬耗時操作:

+ (NSArray<LXDTestsModel *> *)randomModels
{
    NSMutableArray * models = @[].mutableCopy;
    NSArray * names = @[
                    @"SindriLin", @"Bison", @"XiongZengHui", @"ZengChengChun", @"Tessie"
                        ];
    NSArray * ages = @[
                      @15, @20, @25, @30, @35
                      ];
    NSArray * flags = @[
                        @123, @456, @789, @012, @234
                        ];
    for (NSUInteger idx = 0; idx < 100; idx++) {
        LXDTestsModel * model = [LXDTestsModel modelWithName: names[arc4random() % names.count] age: ages[arc4random() % ages.count] flags: [flags[arc4random() % flags.count] unsignedIntegerValue]];
        [models addObject: model];
        [NSThread sleepForTimeInterval: 0.01];
    }
    return models;
}

運行測試用法后控制臺會輸出下面的信息,其中紅框中表示執行代碼總耗時,在此demo中總共運行了11.015秒的時長

性能測試輸出

雖然性能測試的定位確實有些雞肋,但是另一方面,直接使用單元測試來獲取某段代碼的執行時間要比使用instrument快的多。通過性能測試直觀的獲取執行時間后,我們可以根據需要來決定是否將這些代碼放到子線程中執行來優化代碼(很多時候,數據轉換會占用大量的CPU計算資源)

異步測試

由于單元測試是在主線程中進行的,因此異步操作的測試在執行完畢之前,往往已經結束了。為了實現異步測試,筆者采用while()的方式無限循環等待,為了實現這個效果,我在LXDTestsModel頭文件中添加了一個NSData類型的屬性以及一個異步操作的接口方法,通過判斷這個屬性值來實現效果:

- (void)asyncConvertToData
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSDictionary * modelJSON = nil;
        for (NSInteger idx = 0; idx < 20; idx++) {
            modelJSON = [self modelToDictionary];
            [self setValuesWithDictionary: modelJSON];
            [NSThread sleepForTimeInterval: 0.001];
        }
        _data = [NSJSONSerialization dataWithJSONObject: modelJSON options: NSJSONWritingPrettyPrinted error: nil];
    });
}

上面的代碼在系統創建的默認等級的子線程中執行了一段耗時代碼,最后把json轉換成NSData數據保存在自身的屬性中。對應的異步測試代碼如下:

- (void)testAsync
{
    NSDictionary * dict = @{
                          @"name": @"SindriLin",
                          @"age": @22,
                          @"flags": @987654321
                          };
    LXDTestsModel * model = [[LXDTestsModel alloc] initWithDictionary: dict];
    XCTAssertNotNil(model);

    [model asyncConvertToData];
    while (model.data == nil) {
        CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.01, YES);
        NSLog(@"waiting");
    }
    XCTAssertNotNil(model.data);
    NSLog(@"convert finish %@", model.data);
}

同樣的,如果你的異步操作是網絡請求,那么在執行的回調外對獲取的數據類型加上__block修飾,然后判斷這個獲取的數據是否不為空來停止循環。另外最重要的是你必須在你的死循環中加入CFRunLoopRunInModel這個函數的調用來保證即便是在等待的情況下,你的主線程仍然能處理其他的事情。

__block BOOL complete = NO;
__block NSData * data = nil;
[network POST: @"http://xxxxxxx" parameters: nil completion: ^(NSData * receiveData) {
    data = receiveData;
    complete = YES:
}];

while (!complete) {
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.01, YES);
    NSLog(@"requesting");
}

尾言

最開始筆者一度認為單元測試是個比較考驗技術的東西,但恰恰相反的,單元測試的使用與概念是相當簡單的一個東西,難點在于不知道怎么用,這就需要我們持續的使用練習才能更好的服務于我們的開發。此外,常用的第三方框架例如YYModelAFNetworkingAlamofire等等優秀框架中也有對框架自身編寫的單元測試,學習仿寫這些單元測試也是快速提升自己的一種手段。

很多時候,我們的項目中難免發生多個類之間的交互處理,而這種操作非常的不好調試。單元測試的原則之一就在于我們用來測試的代碼要求功能很單一,這其實與良好的代碼設計的思想是非常相符的。一方面來說,良好的代碼結構設計可以讓我們的測試用例的構建更加快速簡單;反過來單元測試逼著我們去想辦法減少類之間的耦合以此來減少甚至排除測試的干擾。無論如何,如果你想成為更好的開發者,單元測試是我們快速提升代碼認知的重要手段之一。

文集:iOS開發

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內容

  • 單元測試就是為你的方法多專門寫一個測試函數。以保證你的方法在不停的修改開發中。保持正確。如果出錯,第一時間讓你知道...
    zhaihongxia閱讀 660評論 0 0
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,739評論 25 708
  • 圣誕大戰(2) “好咧,走起。”說完,我們就發動車子。 “范主任,現在我們四個人,說多不多,說少也不少,我建議去之...
    順哥愛飆車閱讀 371評論 1 0
  • 在我開始有點名氣,漸漸被人熟知的那段時間,有一個案子給我留下了極其深刻的印象,不僅因為它是我作為業余偵探以來第一次...
    芝士尾巴閱讀 261評論 1 2
  • 【lsyncd 工具介紹】 如果想自動同步兩個目錄下的所有文件,讓兩個或多個目錄保持數據完全一致,大多數情況下就需...
    zwsuo閱讀 2,481評論 0 1