鍍金池/ 教程/ iOS/ 測(cè)試并發(fā)程序
與四軸無(wú)人機(jī)的通訊
在沙盒中編寫(xiě)腳本
結(jié)構(gòu)體和值類(lèi)型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測(cè)試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動(dòng)開(kāi)發(fā)
Collection View 動(dòng)畫(huà)
截圖測(cè)試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動(dòng)布局工具箱
動(dòng)畫(huà)
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類(lèi)
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動(dòng)畫(huà)解釋
響應(yīng)式 Android 應(yīng)用
初識(shí) TextKit
客戶(hù)端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測(cè)試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動(dòng)游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動(dòng)畫(huà)
常見(jiàn)的后臺(tái)實(shí)踐
糟糕的測(cè)試
避免濫用單例
數(shù)據(jù)模型和模型對(duì)象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場(chǎng)
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類(lèi)
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線(xiàn)程安全類(lèi)的設(shè)計(jì)
置換測(cè)試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無(wú)人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測(cè)試
值對(duì)象
活動(dòng)追蹤
依賴(lài)注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測(cè)試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場(chǎng)
游戲
調(diào)試核對(duì)清單
View Controller 容器
學(xué)無(wú)止境
XCTest 測(cè)試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動(dòng)畫(huà)
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲(chǔ)
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺(jué)
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫(kù)支持
Fetch 請(qǐng)求
導(dǎo)入大數(shù)據(jù)集
iOS 開(kāi)發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語(yǔ)言標(biāo)簽
同步案例學(xué)習(xí)
依賴(lài)注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識(shí)別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過(guò)程

測(cè)試并發(fā)程序

在開(kāi)發(fā)高質(zhì)量應(yīng)用程序的過(guò)程中,測(cè)試是一個(gè)很重要的工具。在過(guò)去,當(dāng)并發(fā)并不是應(yīng)用程序架構(gòu)中重要組成部分的時(shí)候,測(cè)試就相對(duì)簡(jiǎn)單。隨著這幾年的發(fā)展,使用并發(fā)設(shè)計(jì)模式已愈發(fā)重要了,想要測(cè)試好并發(fā)應(yīng)用程序,已成了一個(gè)不小的挑戰(zhàn)。

測(cè)試并發(fā)代碼最主要的困難在于程序或信息流不是反映在調(diào)用堆棧上。函數(shù)并不會(huì)立即返回結(jié)果給調(diào)用者,而是通過(guò)回調(diào)函數(shù),Block,通知或者一些類(lèi)似的機(jī)制,這些使得測(cè)試變得更加困難。

然而,測(cè)試異步代碼也會(huì)帶來(lái)一些好處,比如可以揭露較差的程序設(shè)計(jì),讓最終的實(shí)現(xiàn)變得更加清晰。

異步測(cè)試的問(wèn)題

首先,我們來(lái)看一個(gè)簡(jiǎn)單的同步單元測(cè)試?yán)?。兩個(gè)數(shù)求和的方法:

+ (int)add:(int)a to:(int)b {
    return a + b;
}

測(cè)試這個(gè)方法很簡(jiǎn)單,只需要比較該方法返回的值是否與期望的值相同,如果不相同,則測(cè)試失敗。

- (void)testAddition {
    int result = [Calculator add:2 to:2];
    STAssertEquals(result, 4, nil);
}

接下來(lái),我們利用 Block 將該方法改成異步返回結(jié)果。為了模擬測(cè)試失敗,我們會(huì)在方法實(shí)現(xiàn)中故意添加一個(gè) bug。

+ (int)add:(int)a to:(int)b block:(void(^)(int))block {
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        block(a - b); // 帶有bug的實(shí)現(xiàn)
    }];
}

顯然這是一個(gè)人為的例子,但是它卻真實(shí)的反應(yīng)了在編程中可能經(jīng)常遇到的問(wèn)題,只不過(guò)實(shí)際過(guò)程更復(fù)雜罷了。

測(cè)試上面的方法最簡(jiǎn)單的做法就是把斷言放到 Block 中。盡管我們的方法實(shí)現(xiàn)中存在 bug,但是這種測(cè)試永遠(yuǎn)不會(huì)失敗的:

// 千萬(wàn)不要使用這些代碼!
- (void)testAdditionAsync {
    [Calculator add:2 to:2 block:^(int result) {
        STAssertEquals(result, 4, nil); // 永遠(yuǎn)不會(huì)被調(diào)用到
    }];
}

這里的斷言為什么沒(méi)失敗呢?

關(guān)于SenTestingKit

XCode4 所使用的測(cè)試框架是基于 OCUnit。為了理解之前所提到的異步測(cè)試問(wèn)題,我們需要了解一下測(cè)試包中的各個(gè)部分之間的執(zhí)行順序。下圖展示了一個(gè)簡(jiǎn)化的流程。

SenTestingKit call stack

在測(cè)試框架在主 run loop 開(kāi)始運(yùn)行之后,主要執(zhí)行了以下幾個(gè)步驟:

  1. 配置一個(gè)包含所有相關(guān)測(cè)試的測(cè)試包 (比如可以在工程的 scheme 中配置)。
  2. 運(yùn)行測(cè)試包,內(nèi)部會(huì)調(diào)用所有以 test 開(kāi)頭測(cè)試用例的方法。運(yùn)行結(jié)束后會(huì)返回一個(gè)包含單個(gè)測(cè)試結(jié)果的對(duì)象。
  3. 調(diào)用 exit() 退出測(cè)試。

這其中我們最感興趣的是單個(gè)測(cè)試是如何被調(diào)用的。在異步測(cè)試中,包含斷言的 Block 會(huì)被加到主 run loop。當(dāng)所有的測(cè)試執(zhí)行完畢后,測(cè)試框架就會(huì)退出,而 block 卻從來(lái)沒(méi)有被執(zhí)行,因此不會(huì)引起測(cè)試失敗。

當(dāng)然我們有很多種方發(fā)來(lái)解決這個(gè)問(wèn)題。但是所有的方法都必須在主 run loop 中運(yùn)行,而且在測(cè)試方法返回和比較結(jié)果之前需要處理已入隊(duì)所有操作。

Kiwi 使用探測(cè)輪詢(xún) (probe poller),它可以在測(cè)試方法中被調(diào)用。 GHUnit 編寫(xiě)了一個(gè)單獨(dú)的測(cè)試類(lèi),它必須在測(cè)試的方法內(nèi)初始化,并在結(jié)束時(shí)接收一個(gè)通知。以上兩種方式都是通過(guò)編寫(xiě)相應(yīng)的代碼來(lái)確保異步測(cè)試方法在測(cè)試結(jié)束之前都不會(huì)返回。

SenTestingKit的異步擴(kuò)展

我們對(duì)這個(gè)問(wèn)題的解決方案是對(duì) SenTestingKit 添加一個(gè)擴(kuò)展,它在棧上使用同步執(zhí)行,并把每個(gè)部分加入到主隊(duì)列上。正如下圖所見(jiàn),在驗(yàn)證整個(gè)測(cè)試框架結(jié)果之前,報(bào)告異步測(cè)試成功或者失敗的 Block 就被加入到隊(duì)列。這種執(zhí)行順序允許我們開(kāi)啟一個(gè)測(cè)試并等待它的測(cè)試結(jié)果。

SenTestingKitAsync call stack

如果測(cè)試方法以 Async 結(jié)尾,框架就會(huì)認(rèn)為該方法是異步測(cè)試。此外,在異步測(cè)試中,我們必須手動(dòng)地報(bào)告測(cè)試成功,同時(shí)為了防止 Block 永遠(yuǎn)不會(huì)被調(diào)用,我們還需添加了一個(gè)超時(shí)方法。之前的錯(cuò)誤的測(cè)試方法修改后如下所示:

- (void)testAdditionAsync {
    [Calculator add:2 to:2 block^(int result) {
        STAssertEquals(result, 4, nil);
        STSuccess(); // 通過(guò)調(diào)用這個(gè)宏來(lái)判斷是否測(cè)試成功
    }];
    STFailAfter(2.0, @"Timeout");
}

設(shè)計(jì)異步測(cè)試

就像同步測(cè)試一樣,異步測(cè)試也應(yīng)該比被測(cè)試的功能簡(jiǎn)單許多。復(fù)雜的測(cè)試并不會(huì)改進(jìn)代碼的質(zhì)量,反而會(huì)給測(cè)試本身帶來(lái)更多的 Bug。在以測(cè)試驅(qū)動(dòng)開(kāi)發(fā)的情況下,簡(jiǎn)單的測(cè)試會(huì)讓我們對(duì)組件,接口以及架構(gòu)的行為有更清醒的認(rèn)識(shí)。

示例工程

為了運(yùn)用到實(shí)際中,我們創(chuàng)建了一個(gè)示例框架:PinacotecaCore,它從一個(gè)虛擬的服務(wù)器獲取圖像信息??蚣苤邪粋€(gè)資源管理器,它對(duì)外提供一個(gè)可以根據(jù)圖像 Id 獲取圖像對(duì)象的接口。該接口的工作原理是資源管理器從虛擬服務(wù)器獲取圖片對(duì)象的信息,并更新到數(shù)據(jù)庫(kù)。

雖然這個(gè)示例框架只是為了演示,但在我們自己開(kāi)發(fā)的許多應(yīng)用中也使用了這種模式。

PinacotecaCore architecture

從上圖我們可以知道,示例框架有三個(gè)組件我們需要測(cè)試:

  1. 模型層
  2. 模擬服務(wù)器請(qǐng)求的服務(wù)器接口控制器(API Controller)
  3. 管理 core data 堆棧以及連接模型層和服務(wù)接口控制器的資源管理器

模型層

測(cè)試應(yīng)該盡量使用同步的方式進(jìn)行,而模型層就是一個(gè)很好的實(shí)例。只要不同的被托管對(duì)象上下文 (managed object contexts) 之間沒(méi)有復(fù)雜的依賴(lài)關(guān)系,測(cè)試用例都應(yīng)該根據(jù)上下文在主線(xiàn)程上設(shè)置它自己的 core data 堆棧,并在其中執(zhí)行各自的操作。

在這個(gè)測(cè)試實(shí)例中,我們就是在 setUp 方法中設(shè)置 core data 堆棧,然后檢查 PCImage 實(shí)體的描述是否存在,如果不存在就構(gòu)造一個(gè),并更新它的值。當(dāng)然這和異步測(cè)試沒(méi)有關(guān)系,我們就不深入細(xì)說(shuō)了。

服務(wù)器接口控制器

框架中的第二個(gè)組件就是服務(wù)器接口控制器。它主要處理服務(wù)器請(qǐng)求以及服務(wù)器 API 到模型的映射關(guān)系。讓我們來(lái)看一下下面這個(gè)方法:

- [PCServerAPIController fetchImageWithId:queue:completionHandler:]

調(diào)用它需要三個(gè)形參:一個(gè)圖片對(duì)象 Id,所在的執(zhí)行隊(duì)列以及一個(gè)完成后的回調(diào)方法。

因?yàn)榉?wù)器根本不存在,一個(gè)比較好的做法就是偽造一個(gè)代理服務(wù)器,正好 OHHTTPStubs 可以解決這個(gè)問(wèn)題。在它的最新版本中,可以在示例的請(qǐng)求響應(yīng)中包含一個(gè) bundle,發(fā)送給客戶(hù)端。

為了能 stub 請(qǐng)求,OHHTTPStubs 需要在測(cè)試類(lèi)初始化時(shí)或者 setUp 方法中進(jìn)行配置。首先,我們需要加載一個(gè)包含請(qǐng)求響應(yīng)對(duì)象(response)的 bundle:

NSURL *url = [[NSBundle bundleForClass:[self class]]
                        URLForResource:@"ServerAPIResponses"
                         withExtension:@"bundle"];

NSBundle *bundle = [NSBundle url];

然后我們從 bundle 加載 response 對(duì)象,作為請(qǐng)求的響應(yīng)值:

OHHTTPStubsResponse *response;
response = [OHHTTPStubsResponse responseNamed:@"images/123"
                                   fromBundle:responsesBundle
                                 responseTime:0.1];

[OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
    return YES /* 如果所返回的request是我們所期望的,就返回YES */;
} withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) {
    return response;
}];

通過(guò)如上的設(shè)置之后,簡(jiǎn)化版的測(cè)試服務(wù)器接口控制器如下:

- (void)testFetchImageAsync
{
    [self.server
        fetchImageWithId:@"123"
                   queue:[NSOperationQueue mainQueue]
       completionHandler:^(id imageData, NSError *error) {
          STAssertEqualObjects([NSOperationQueue currentQueue], queue, nil);
          STAssertNil(error, [error localizedDescription]);
          STAssertTrue([imageData isKindOfClass:[NSDictionary class]], nil);

          // 檢查返回的字典中的值.

          STSuccess();
       }];
    STFailAfter(2.0, nil);    
}

資源管理器

最后一個(gè)部分是資源管理器,它不但把服務(wù)器接口控制器和模型層聯(lián)系起來(lái), 還管理著 core data 堆棧。下面我們想測(cè)試獲取一個(gè)圖片對(duì)象的方法:

-[PCResourceManager imageWithId:usingManagedObjectContext:queue:updateHandler:]

該方法根據(jù) id 返回一個(gè)圖片對(duì)象。如果圖片在數(shù)據(jù)庫(kù)中不存在,它會(huì)創(chuàng)建一個(gè)只包含 id 的新對(duì)象,然后通過(guò)服務(wù)器接口控制器獲取圖片對(duì)象的詳細(xì)信息。

由于資源管理器的測(cè)試不應(yīng)該依賴(lài)于服務(wù)器接口控制器,所以我們可以用 OCMock 來(lái)模擬,如果要做方法的部分 stub,它是一個(gè)理想的框架。如以下的 資源管理器測(cè)試 :

OCMockObject *mo;
mo = [OCMockObject partialMockForObject:self.resourceManager.server];

id exp = [[serverMock expect] 
             andCall:@selector(fetchImageWithId:queue:completionHandler:)
            onObject:self];
[exp fetchImageWithId:OCMOCK_ANY queue:OCMOCK_ANY completionHandler:OCMOCK_ANY];

上面的代碼實(shí)際上它并沒(méi)有真正調(diào)用服務(wù)器接口控制器的方法,而是調(diào)用我們寫(xiě)在測(cè)試類(lèi)中的方法。

用上面的做法,對(duì)資源管理的測(cè)試就變得很直觀。當(dāng)我們調(diào)用資源管理器獲取資源時(shí),實(shí)際上調(diào)用的是我們模擬的服務(wù)器接口控制器的方法。這樣我們也能檢查調(diào)用服務(wù)器接口控制器時(shí)參數(shù)是否正確。在調(diào)用了獲取圖像對(duì)象的方法后,資源管理器會(huì)更新模型,然后調(diào)用驗(yàn)證測(cè)試成功與否的宏。

- (void)testGetImageAsync
{
    NSManagedObjectContext *ctx = self.resourceManager.mainManagedObjectContext;
    __block PCImage *img;
    img = [self.resourceManager imageWithId:@"123"
                  usingManagedObjectContext:ctx
                                      queue:[NSOperationQueue mainQueue]
                              updateHandler:^(NSError *error) {
                                       // 檢查error是否為空以及image是否已經(jīng)被更新 
                                       STSuccess();
                                   }];    
    STAssertNotNil(img, nil);
    STFailAfter(2.0, @"Timeout");
}

總結(jié)

剛開(kāi)始時(shí)候,使用并發(fā)設(shè)計(jì)模式測(cè)試應(yīng)用程序是具有一定的挑戰(zhàn)性,但是一旦你理解了它們的不同,并建立最佳實(shí)踐,一切都會(huì)變得簡(jiǎn)單而有趣。

nxtbgthng 項(xiàng)目中,我們用 SenTestingKitAsync 框架來(lái)測(cè)試。但是像 KiwiGHUnit 也都是不錯(cuò)的異步測(cè)試框架。建議你都可以嘗試下,然后找到合適自己的測(cè)試工具并開(kāi)始使用它。