用 NSURProtocol 注入測(cè)試數(shù)據(jù)

在之前的幾篇博文中晶渠,筆者介紹過訪問異步網(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 官方博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末惯裕,一起剝皮案震驚了整個(gè)濱河市绣硝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌握玛,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冕屯,死亡現(xiàn)場(chǎng)離奇詭異拂苹,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)搞挣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來仓犬,“玉大人,你說我怎么就攤上這事窘面∵辞” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵酣难,是天一觀的道長(zhǎng)黑滴。 經(jīng)常有香客問我,道長(zhǎng)菜谣,這世上最難降的妖魔是什么晚缩? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮冈敛,結(jié)果婚禮上卿泽,老公的妹妹穿的比我還像新娘滋觉。我一直安慰自己齐邦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著丐吓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪术健。 梳的紋絲不亂的頭發(fā)上粘衬,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音勘伺,去河邊找鬼褂删。 笑死,一個(gè)胖子當(dāng)著我的面吹牛缅帘,可吹牛的內(nèi)容都是我干的蹲盘。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼铃诬,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼苍凛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起宣肚,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤悠栓,失蹤者是張志新(化名)和其女友劉穎按价,沒想到半個(gè)月后笙瑟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體往枷,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年秉宿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了屯碴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡酌摇,死狀恐怖嗡载,靈堂內(nèi)的尸體忽然破棺而出洼滚,到底是詐尸還是另有隱情技潘,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布铲掐,位于F島的核電站值桩,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏奔坟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一婉支、第九天 我趴在偏房一處隱蔽的房頂上張望澜建。 院中可真熱鬧,春花似錦何之、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至舰褪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間略就,已是汗流浹背晃酒。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工贝次, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蛔翅。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓山析,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親笋轨。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 注:文章翻譯自網(wǎng)絡(luò)博客。 Using NSURLProtocol forInjecting Test Data S...
    瞎貓與死耗子閱讀 591評(píng)論 2 0
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,504評(píng)論 25 707
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)等龙,斷路器伶贰,智...
    卡卡羅2017閱讀 134,599評(píng)論 18 139
  • 我有一顆善良的種子 人卻始終沒長(zhǎng)大 我在南方的海灘上 拾起黍衙,一串貝殼 暗色的寶,去向 沒有回憶的城市 走進(jìn)幽谷的小...
    余溫好似涼白開閱讀 165評(píng)論 0 0
  • 先聲明這是我第一次寫影評(píng)琅翻,抱歉可能會(huì)寫的不太好柑贞,但是看在我晚上11點(diǎn)半仍然堅(jiān)持寫的份上,希望看的朋友寬容些棠众。 很喜...
    窗子閱讀 1,241評(píng)論 3 2