在以前的幾篇博文中,筆者介紹過訪問異步網絡的單元測試方法及如何使用模擬對象來進一步控制單元測試的範圍。在今天的教程中,筆者將展現另外一種方法,即:經過自定義 NSURProtocol 類來獲取靜態測試數據,從而爲測試提供可靠的數據。html
幾個月前,Gowalla 在 GitHub 上公開了他們用於 iPhone 客戶端的網絡代碼。這個被稱爲 AFNetworking 的庫,是一個「使用 NSOperations 和 block 回調的、討喜的 iOS 網絡庫」。這段代碼中首先吸引筆者的一點,是利用該庫內置的支持服務,僅需幾行代碼便可訪問基於 JSON 的服務。json
AFNetworking 的界面之簡潔,啓發筆者運行一次快速的測試,並編寫ILBitly。ILBitly 可提供一個基於 Objective C 的包裝類,從而得到 Bitly 的 URL 縮短服務。AFNetworking 的使用很是簡單,尤爲是 JSON 的支持服務,僅需調用單個類的方法便可得到。然而,這簡潔性也爲咱們使用 MCMock 編寫自包含單元和模擬測試增添了很多難度。這主要是由於 OCMock 不支持類方法的模擬。筆者也嘗試過其它方法,例如 method swizzling,然而並無成功。api
就在幾天前,筆者看到 GitHub 上的一則討論,有關如何恰當地模擬 AFNetworking 的接口。討論中 Adam Ernst 建議使用自定義的 NSURLProtocol 來完成這項任務。這讓筆者靈光一現,終於想到了解決測試問題的方法。性能優化
如上文所述,筆者須要攔截網絡訪問,但當時找不到一種簡單的方法來模擬 AFJSONRequestOperation 的接口。因而想到了另外一條路,即攔截 iOS 內置的標準 http 協議。這能夠經過註冊自定義的NSURLProtocol 子類 ILCannedURLProtocol 來實現。該子類可處理 http 請求。因爲詢問協議處理器的順序與註冊順序是相反的。所以相較於標準類,咱們的類老是會被優先訪問。網絡
這樣作的主要目的,是每當出現一個 http 請求,ILCannedURLProtocol 即會迴應一組預先加載好的測試數據。如此一來,咱們就能在測試中消除全部外部影響。同時,能夠在須要時,故意使 http 請求失敗。ILCannedURLProtocol 的接口以下所示:app
@interface ILCannedURLProtocol : NSURLProtocol + (void)setCannedResponseData:(NSData*)data; + (void)setCannedHeaders:(NSDictionary*)headers; + (void)setCannedStatusCode:(NSInteger)statusCode; + (void)setCannedError:(NSError*)error; @end
在現有 http 請求的形式下,咱們不能替換任何一個請求的所有內容。舉例來講,咱們只能攔截 GET 請求,卻沒法攔截任何類型的權限認證質詢(authentication challenge)或認證應答(authentication response)。但它現有的功能已經足覺得測試 ILBitly 及其它類似的類提供測試數據。curl
基本上每一個 setCannedXxx 方法都會保留傳給它的對象,所以每當http 請求須要時,能夠返回這些對象。但這也意味着它們只能每次應對一組測試數據。異步
子類化 NSURLProtocol 還須要實現一些其餘的方法。其中之一是canInitWithRequest:每當發起一個 NSURLRequest 時,都會調用該方法,來判斷該類是否支持這一請求。咱們將使用這個方法來攔截 http GET 請求:oop
+ (BOOL)canInitWithRequest:(NSURLRequest *)request { // For now only supporting http GET return [[[request URL] scheme] isEqualToString:@"http"] && [[request HTTPMethod] isEqualToString:@"GET"]; }
同時咱們也須要實現 startLoading 方法。該方法會在每次實例化相關協議處理器時被調用,從而給請求提供數據。根據設置的封裝數據不一樣,咱們的方法將會給出一個成功的迴應,或者報出一個錯誤:性能
- (void)startLoading { NSURLRequest *request = [self request]; id client = [self client]; if(gILCannedResponseData) { // Send the canned data NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:[request URL] statusCode:gILCannedStatusCode headerFields:gILCannedHeaders requestTime:0.0]; [client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; [client URLProtocol:self didLoadData:gILCannedResponseData]; [client URLProtocolDidFinishLoading:self]; [response release]; } else if(gILCannedError) { // Send the canned error [client URLProtocol:self didFailWithError:gILCannedError]; } }
若是你決定在本身的項目中使用上述代碼測試,當心不要把它寫入任何打算上傳到 APP Store 的產品代碼中去。若是你不明白爲何,讓咱們來看一下 NSHTTPURLResponse 的初始化程序。這是一個私有 API,經過在 iOS 4.3 SDK 上運行 class-dump 來獲取。若是你把這段回調加在產品代碼中,蘋果可能會拒絕它。蘋果甚至可能會在將來的 iOS更新中對它進行修改,儘管可能性不大。 但若是隻是用它來跑單元測試的話,那應該沒什麼問題。
除去另外幾個基本爲空的方法,全部的方法都在這了。如今只需註冊咱們自定義的類,而後再加載一些封裝數據進去。
The unit test class for ILBitly just includes a few instance variables:
@interface ILBitlyTest : SenTestCase { ILBitly *bitly; id bitlyMock; BOOL done; } @end
變量 bitly 包含 test下ILBitly 代碼的一個實例,bitlyMock 包含了用做 ILBitly 測試的部分 mock 對象,done 是異步調用結束的信號。後面筆者會詳細地解釋這些變量。
執行每一個測試用例以前,setUp 方法都會被自動調用,來作如下準備:
- (void)setUp { [super setUp]; // Init bitly proxy using test id and key - not valid for real use bitly = [[ILBitly alloc] initWithLogin:@"LOGIN" apiKey:@"KEY"]; done = NO; [NSURLProtocol registerClass:[ILCannedURLProtocol class]]; [ILCannedURLProtocol setCannedStatusCode:200]; }
咱們這個方法來準備默認的測試實例,以及註冊ILCannedURLProtocol。那些用來實例化 ILBitly 的參數只是傳給服務請求的佔位符。由於以後咱們會使用靜態測試數據,因此它們其實並無什麼實際用途,僅供稍後確認它們是否被如期傳遞。
爲了平衡資源,每次測試後,咱們都會註銷自定義協議,同時銷燬測試數據。
- (void)tearDown { [NSURLProtocol unregisterClass:[ILCannedURLProtocol class]]; [ILCannedURLProtocol setCannedHeaders:nil]; [ILCannedURLProtocol setCannedResponseData:nil]; [ILCannedURLProtocol setCannedError:nil]; [bitly release]; bitlyMock = nil; [super tearDown]; }
咱們也須要準備一些測試數據。這很容易:如上一篇博文所說,咱們能夠用 curl 來保存從 bitly 到 JSON 文件的原始應答,而後在每一個測試用例中加載出來。
最後,咱們寫些測試來驗證 ILBitly 代碼。例如,下文是一個驗證縮短 URL 服務的測試:
- (void)testShorten { // Prepare the canned test result [ILCannedURLProtocol setCannedResponseData:[self cannedDataWithName:@"shorten"]]; [ILCannedURLProtocol setCannedHeaders: [NSDictionary dictionaryWithObject:@"application/json; charset=utf-8" forKey:@"Content-Type"]]; // Prepare the mock bitlyMock = [OCMockObject partialMockForObject:bitly]; NSURL *trigger = [NSURL URLWithString:@"http://"]; [[[bitlyMock expect] andReturn:[NSURLRequest requestWithURL:trigger]] requestForURLString:[OCMArg checkWithBlock:^(id url) { return [url isEqualToString:EXPECTED_REQUEST]; }]]; // Execute the code under test [bitly shorten:@"http://www.infinite-loop.dk/blog/" result:^(NSString *result) { STAssertEqualObjects(result, @"http://j.mp/qA7S4Q", @"Unexpected short url"); done = YES; } error:^(NSError *err) { STFail(@"Shorten failed with error: %@", [err localizedDescription]); done = YES; }]; // Verify the result STAssertTrue([self waitForCompletion:5.0], @"Timeout"); [bitlyMock verify]; }
在第一部分中,靜態測試數據被加載到測試協議中。
以後咱們爲 bitly 對象建立了部分模擬對象。它的主要功能是攔截對requestForURLString 的內部調用,並建立一個咱們指望調用的 URL。調用時,測試會驗證是否向咱們指望的URL發出了請求,並最終返回一個 NSURLRequest 實例。爲觸發加載咱們自定義的協議,該實例只包含了基本的 URL Scheme。
被測試的代碼可如第三部分所示被執行。因爲調用(invoke) shorten:result:error後,block 隨時可能被回調,咱們設置了done,這樣一來調用時咱們就能知道了。
如上一篇博文所述,最後的一段代碼將會給 done 信號最多 5 秒的等待時間。最後,確認模擬對象被調回,從而確認已經收到了所指望的信息。
若是咱們轉而想測試系統對錯誤的處理,咱們只需替換掉測試方法的第一部分,改成錯誤數據,同時相應地對測試作以下改動:
[ILCannedURLProtocol setCannedError: [NSError errorWithDomain:NSURLErrorDomain code:kCFURLErrorTimedOut userInfo:nil]];
綜上所述,咱們能夠利用 NSURLProtocol 將可預測的測試數據注入單元測試和模擬測試中,以減小外部因素的影響。咱們甚至能夠擴展這些測試。舉例來講,你能夠用這個方法模擬糟糕的網絡環境,如長延遲和窄帶寬。可能性是無窮的,筆者僅但願可用此文拋磚引玉。
本文中所使用的 ILBitly 包及測試類均可在 GitHub 上找到,同時筆者還放了一個 iPhone APP 樣例,用以演示某些功能。
更新:ILCannedURLProtocol 類也已放到 Github的 ILTesting 庫中。
針對如今的信息就是作的處理。
歡迎各種評論與建議。原文地址:http://www.infinite-loop.dk/blog/2011/09/using-nsurlprotocol-for-injecting-test-data/
OneAPM Mobile Insight ,監控網絡請求及網絡錯誤,提高用戶留存。訪問 OneAPM 官方網站感覺更多應用性能優化體驗,想閱讀更多技術文章,請訪問 OneAPM 官方技術博客。
本文轉自 OneAPM 官方博客