在之前的幾篇博文中晶渠,筆者介紹過訪問異步網(wǎng)絡(luò)的單元測(cè)試方法及如何使用模擬對(duì)象來進(jìn)一步控制單元測(cè)試的范圍搔预。在今天的教程中资锰,筆者將展示另一種方法,即:通過自定義 NSURProtocol 類來獲取靜態(tài)測(cè)試數(shù)據(jù)寡喝,從而為測(cè)試提供可靠的數(shù)據(jù)糙俗。
幾個(gè)月前,Gowalla 在 GitHub 上公開了他們用于 iPhone 客戶端的網(wǎng)絡(luò)代碼预鬓。這個(gè)被稱為 AFNetworking 的庫巧骚,是一個(gè)「使用 NSOperations 和 block 回調(diào)的、討喜的 iOS 網(wǎng)絡(luò)庫」格二。這段代碼中首先吸引筆者的一點(diǎn)劈彪,是利用該庫內(nèi)置的支持服務(wù),僅需幾行代碼即可訪問基于 JSON 的服務(wù)顶猜。
AFNetworking 的界面之簡(jiǎn)潔沧奴,啟發(fā)筆者運(yùn)行一次快速的測(cè)試,并編寫ILBitly长窄。ILBitly 可提供一個(gè)基于 Objective C 的包裝類滔吠,從而獲得 Bitly 的 URL 縮短服務(wù)。AFNetworking 的使用非常簡(jiǎn)單挠日,尤其是 JSON 的支持服務(wù)疮绷,僅需調(diào)用單個(gè)類的方法即可獲得。然而嚣潜,這簡(jiǎn)潔性也為我們使用 MCMock 編寫自包含單元和模擬測(cè)試增添了不少難度冬骚。這主要是因?yàn)?OCMock 不支持類方法的模擬。筆者也嘗試過其它方法懂算,例如 method swizzling只冻,然而并沒有成功计技。
就在幾天前,筆者看到 GitHub 上的一則討論酸役,有關(guān)如何恰當(dāng)?shù)啬M AFNetworking 的接口驾胆。討論中 Adam Ernst 建議使用自定義的 NSURLProtocol 來完成這項(xiàng)任務(wù)涣澡。這讓筆者靈光一現(xiàn),終于想到了解決測(cè)試問題的方法入桂。
子類化 NSURLProtocol
如上文所述,筆者需要攔截網(wǎng)絡(luò)訪問抗愁,但當(dāng)時(shí)找不到一種簡(jiǎn)單的方法來模擬 AFJSONRequestOperation 的接口馁蒂。于是想到了另一條路,即攔截 iOS 內(nèi)置的標(biāo)準(zhǔn) http 協(xié)議蜘腌。這可以通過注冊(cè)自定義的NSURLProtocol 子類 ILCannedURLProtocol 來實(shí)現(xiàn)沫屡。該子類可處理 http 請(qǐng)求撮珠。由于詢問協(xié)議處理器的順序與注冊(cè)順序是相反的。因此相較于標(biāo)準(zhǔn)類勺届,我們的類總是會(huì)被優(yōu)先訪問娶耍。
這樣做的主要目的,是每當(dāng)出現(xiàn)一個(gè) http 請(qǐng)求榕酒,ILCannedURLProtocol 即會(huì)回應(yīng)一組預(yù)先加載好的測(cè)試數(shù)據(jù)。如此一來澜掩,我們就能在測(cè)試中消除所有外部影響杖挣。同時(shí),可以在需要時(shí)惩妇,故意使 http 請(qǐng)求失敗。ILCannedURLProtocol 的接口如下所示:
@interface ILCannedURLProtocol : NSURLProtocol
+ (void)setCannedResponseData:(NSData*)data;
+ (void)setCannedHeaders:(NSDictionary*)headers;
+ (void)setCannedStatusCode:(NSInteger)statusCode;
+ (void)setCannedError:(NSError*)error;
@end
在現(xiàn)有 http 請(qǐng)求的形式下乔妈,我們不能替換任何一個(gè)請(qǐng)求的全部?jī)?nèi)容氓皱。舉例來說,我們只能攔截 GET 請(qǐng)求股淡,卻無法攔截任何類型的權(quán)限認(rèn)證質(zhì)詢(authentication challenge)或認(rèn)證應(yīng)答(authentication response)廷区。但它現(xiàn)有的功能已經(jīng)足以為測(cè)試 ILBitly 及其它相似的類提供測(cè)試數(shù)據(jù)唯灵。
基本上每個(gè) setCannedXxx 方法都會(huì)保留傳給它的對(duì)象隙轻,因此每當(dāng)http 請(qǐng)求需要時(shí)垢揩,可以返回這些對(duì)象敛瓷。但這也意味著它們只能每次應(yīng)對(duì)一組測(cè)試數(shù)據(jù)琐驴。
子類化 NSURLProtocol 還需要實(shí)現(xiàn)一些其他的方法。其中之一是canInitWithRequest:每當(dāng)發(fā)起一個(gè) NSURLRequest 時(shí)绝淡,都會(huì)調(diào)用該方法,來判斷該類是否支持這一請(qǐng)求悬包。我們將使用這個(gè)方法來攔截 http GET 請(qǐng)求:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
// For now only supporting http GET
return [[[request URL] scheme] isEqualToString:@"http"]
&& [[request HTTPMethod] isEqualToString:@"GET"];
}
同時(shí)我們也需要實(shí)現(xiàn) startLoading 方法馍乙。該方法會(huì)在每次實(shí)例化相關(guān)協(xié)議處理器時(shí)被調(diào)用,從而給請(qǐng)求提供數(shù)據(jù)撑瞧。根據(jù)設(shè)置的封裝數(shù)據(jù)不同显蝌,我們的方法將會(huì)給出一個(gè)成功的回應(yīng),或者報(bào)出一個(gè)錯(cuò)誤:
- (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];
}
}
如果你決定在自己的項(xiàng)目中使用上述代碼測(cè)試酬诀,小心不要把它寫入任何打算上傳到 APP Store 的產(chǎn)品代碼中去骆撇。如果你不明白為什么,讓我們來看一下 NSHTTPURLResponse 的初始化程序神郊。這是一個(gè)私有 API,通過在 iOS 4.3 SDK 上運(yùn)行 class-dump 來獲取践宴。如果你把這段回調(diào)加在產(chǎn)品代碼中爷怀,蘋果可能會(huì)拒絕它带欢。蘋果甚至可能會(huì)在未來的 iOS更新中對(duì)它進(jìn)行修改烤惊,盡管可能性不大吁朦。 但如果只是用它來跑單元測(cè)試的話,那應(yīng)該沒什么問題雄右。
除去另外幾個(gè)基本為空的方法纺讲,所有的方法都在這了。現(xiàn)在只需注冊(cè)我們自定義的類逢渔,然后再加載一些封裝數(shù)據(jù)進(jìn)去乡括。
準(zhǔn)備單元測(cè)試
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 代碼的一個(gè)實(shí)例,bitlyMock 包含了用作 ILBitly 測(cè)試的部分 mock 對(duì)象盲赊,done 是異步調(diào)用結(jié)束的信號(hào)敷扫。后面筆者會(huì)詳細(xì)地解釋這些變量。
執(zhí)行每個(gè)測(cè)試用例之前呻澜,setUp 方法都會(huì)被自動(dòng)調(diào)用羹幸,來做以下準(zhǔn)備:
- (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];
}
我們這個(gè)方法來準(zhǔn)備默認(rèn)的測(cè)試實(shí)例,以及注冊(cè)ILCannedURLProtocol栅受。那些用來實(shí)例化 ILBitly 的參數(shù)只是傳給服務(wù)請(qǐng)求的占位符屏镊。因?yàn)橹笪覀儠?huì)使用靜態(tài)測(cè)試數(shù)據(jù),所以它們其實(shí)并沒有什么實(shí)際用途而芥,僅供稍后確認(rèn)它們是否被如期傳遞。
為了平衡資源误辑,每次測(cè)試后,我們都會(huì)注銷自定義協(xié)議翘狱,同時(shí)銷毀測(cè)試數(shù)據(jù)砰苍。
- (void)tearDown
{
[NSURLProtocol unregisterClass:[ILCannedURLProtocol class]];
[ILCannedURLProtocol setCannedHeaders:nil];
[ILCannedURLProtocol setCannedResponseData:nil];
[ILCannedURLProtocol setCannedError:nil];
[bitly release];
bitlyMock = nil;
[super tearDown];
}
我們也需要準(zhǔn)備一些測(cè)試數(shù)據(jù)。這很容易:如上一篇博文所說茬缩,我們可以用 curl 來保存從 bitly 到 JSON 文件的原始應(yīng)答辟癌,然后在每個(gè)測(cè)試用例中加載出來。
動(dòng)手組裝
最后寡夹,我們寫些測(cè)試來驗(yàn)證 ILBitly 代碼厂置。例如,下文是一個(gè)驗(yàn)證縮短 URL 服務(wù)的測(cè)試:
- (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];
}
在第一部分中智绸,靜態(tài)測(cè)試數(shù)據(jù)被加載到測(cè)試協(xié)議中访忿。
之后我們?yōu)?bitly 對(duì)象創(chuàng)建了部分模擬對(duì)象。它的主要功能是攔截對(duì)requestForURLString 的內(nèi)部調(diào)用迹恐,并創(chuàng)建一個(gè)我們期望調(diào)用的 URL卧斟。調(diào)用時(shí),測(cè)試會(huì)驗(yàn)證是否向我們期望的URL發(fā)出了請(qǐng)求锤岸,并最終返回一個(gè) NSURLRequest 實(shí)例板乙。為觸發(fā)加載我們自定義的協(xié)議,該實(shí)例只包含了基本的 URL Scheme晓猛。
被測(cè)試的代碼可如第三部分所示被執(zhí)行。由于調(diào)用(invoke) shorten:result:error后戒职,block 隨時(shí)可能被回調(diào)透乾,我們?cè)O(shè)置了done乳乌,這樣一來調(diào)用時(shí)我們就能知道了。
如上一篇博文所述汉操,最后的一段代碼將會(huì)給 done 信號(hào)最多 5 秒的等待時(shí)間。最后芒篷,確認(rèn)模擬對(duì)象被調(diào)回采缚,從而確認(rèn)已經(jīng)收到了所期望的信息。
如果我們轉(zhuǎn)而想測(cè)試系統(tǒng)對(duì)錯(cuò)誤的處理篡帕,我們只需替換掉測(cè)試方法的第一部分贸呢,改為錯(cuò)誤數(shù)據(jù),同時(shí)相應(yīng)地對(duì)測(cè)試做如下改動(dòng):
[ILCannedURLProtocol setCannedError:
[NSError errorWithDomain:NSURLErrorDomain
code:kCFURLErrorTimedOut
userInfo:nil]];
結(jié)論
綜上所述怔鳖,我們可以利用 NSURLProtocol 將可預(yù)測(cè)的測(cè)試數(shù)據(jù)注入單元測(cè)試和模擬測(cè)試中猜谚,以減少外部因素的影響。我們甚至可以擴(kuò)展這些測(cè)試昌犹。舉例來說览芳,你可以用這個(gè)方法模擬糟糕的網(wǎng)絡(luò)環(huán)境,如長(zhǎng)延遲和窄帶寬「坑牵可能性是無窮的杈笔,筆者僅希望可用此文拋磚引玉。
本文中所使用的 ILBitly 包及測(cè)試類都可在 GitHub 上找到球榆,同時(shí)筆者還放了一個(gè) iPhone APP 樣例禁筏,用以演示某些功能。
更新:ILCannedURLProtocol 類也已放到 Github的 ILTesting 庫中每强。
針對(duì)現(xiàn)在的信息就是做的處理州刽。
歡迎各類評(píng)論與建議。原文地址:http://www.infinite-loop.dk/blog/2011/09/using-nsurlprotocol-for-injecting-test-data/
OneAPM Mobile Insight 脆烟,監(jiān)控網(wǎng)絡(luò)請(qǐng)求及網(wǎng)絡(luò)錯(cuò)誤房待,提升用戶留存。訪問 OneAPM 官方網(wǎng)站感受更多應(yīng)用性能優(yōu)化體驗(yàn)拜鹤,想閱讀更多技術(shù)文章流椒,請(qǐng)?jiān)L問 OneAPM 官方技術(shù)博客。
本文轉(zhuǎn)自 OneAPM 官方博客