GYHttpMock:iOS HTTP請求模擬工具
來自: http://wereadteam.github.io/2016/02/25/GYHttpMock/
GYHttpMock 是剛開源的 iOS 請求模擬工具,用于iOS App網絡層開發,可以截獲指定的 HTTP request,并根據規則,完全替換或部分修改真實的網絡返回數據。
背景
iOS App開發過程中,前臺開發過程通常都是并行進行的,因此難免會出現一些客戶端需要等待后臺開發聯調的情景,等待的過程往往痛若而無奈(后臺被催得痛苦,前端無奈等待)。通常解決辦法是,客戶端在某處 hardcode 網絡返回數據,當然,一不小心,這種測試代碼被提交到了線上也是常有的事情。還有更“高級”一點,通過設置代理,用抓包工具修改網絡數據,但這種效率低得令人抓狂。
引入一個可以模擬網絡請求的工具似乎就可以輕松滿足需求,但實踐證明,“模擬網絡請求”這個需求并不簡單。例如對于全新的業務,后臺如果還沒有數據,前端完全可以根據協議自己制造假數據返回。但是,很多情況下,可能是對已有業務的變更,也就是需要修改后臺已有的業務數據。
業界解決方案
為了滿足開發過程中模擬網絡請求的需求,HttpMock 工具應運而生,目前業界已經有許多不同的實現方式,基本可以分為兩類:
1.自建HTTP Server
可以在本地搭建 HTTP Server 模擬返回客戶端所需要的數據。以 hibri/HttpMock 為例,它就是在本地搭建了一個HTTP Mock Server,然后根據需求返回指定數據。對于不需要模擬的請求,直接到達真實的Server,需要模擬的請求就轉向MockServer。
這種方案的優勢在于可以應用于多平臺,也可以用各種語言來實現。但是局限性在于,要建立一個 HTTP Server,一方面得自己搭建并維護這個 Server,對于使用者的門檻較高,另一方面,使用時需要一邊修改客戶端代碼,一邊切換到Server環境修改返回數據,比較麻煩。此外這種方案只能選擇替換或不替換,無法做到替換某個請求返回的數據。
2.客戶端截獲
客戶端可以在網絡層截獲自己的網絡請求,然后返回指定數據。這種方式實現的 HttpMock 更加靈活,但是不同的客戶端實現方式會完全不一樣。實現原理是 Hook 系統網絡層的請求分發,對于符合規則的 http request 進行攔截,然后用之前定義的數據直接回調給上層,并不發出真實的請求。
iOS 上目前應用比較廣泛的是 OHHTTPStubs 和 Nocilla ,這兩種實現的功能都類似。Nocilla選擇用領域專用語言(DSL)的形式創建模擬請求,更容易理解,但是mock的功能需要應用中主動開啟和關閉,一旦開啟或關閉會影響應用中所有的HTTP請求。OHHTTPStubs 安裝后自動啟動,根據 request 自動判斷是否需要截獲。但目前這些開源庫都未能做到靈活修改網絡返回的數據。
GYHttpMock 優勢
GYHttpMock 采用客戶端截獲的方式,在 Nocilla DSL 特性基礎上,同時學習OHHTTPStubs的自動開啟和識別,實現了 http response 的部分替換功能。具體優勢:
- 支持部分替換 HTTP Response,也就是可以修改真實網絡返回的數據,這是相對于其它 HttpMock 獨有的核心功能。
- 客戶端引入 GYHttpMock 后,只需一行代碼就可以截獲指定請求,并返回所需要的數據。不需服務端支持,也不需要建立本地HTTP Server。
- 支持 NSURLConnection, NSURLSession,AFNetworking 以及所有采用 iOS Cocoa URL 加載方式的網絡框架。
- 支持正則匹配 HTTP Request,這樣一條 httpMock 可以同時支持多個請求。
- mocked response 支持 json 內容的文件。一般情況下,mocked response 直接用 NSString 表達會比較清晰,但是返回內容比較多的情況下,因為轉義符的原因,將內容以 json 格式寫入文件會更容易些。
使用
安裝
直接將 GYHttpMock 的源文件加入項目中即可。也可以通過 CocoaPods 的方式接入。
應用
在需要攔截的請求之前創建正確的mockRequest:
1.創建一個最簡單的 mockRequest。截獲應用中訪問 www.weread.com 的 get 請求,并返回一個 response body為空的數據。
mockRequest(@"GET", @"http://www.weread.com");
</div>
2.創建一個攔截條件更復雜的 mockRequest。截獲應用中 url 包含 weread.com,而且包含了 name=abc 的參數
mockRequest(@"GET", @"(.*?)weread.com(.*?)".regex). withBody(@"{\"name\":\"abc\"}".regex);
</div>
3.創建一個指定返回數據的 mockRequest。withBody的值也可以是某個 xxx.json 文件,不過這個 json 文件需要加入到項目中。
mockRequest(@"POST", @"http://www.weread.com"). withBody(@"{\"name\":\"abc\"}".regex); andReturn(200). withBody(@"{\"key\":\"value\"}");
</div>
4.創建一個修改部分返回數據的 mockRequest。這里會根據 weread.json 的內容修改正常網絡返回的數據
mockRequest(@"POST", @"http://www.weread.com"). isUpdatePartResponseBody(YES). withBody(@"{\"name\":\"abc\"}".regex); andReturn(200). withBody(@“weread.json");
</div>
假設正常網絡返回的原始數據是這樣:
{"data": [ { "bookId":"0000001", "updated": [ { "chapterIdx": 1, "title": "序言", }, { "chapterIdx": 2, "title": "第2章", } ] }]}
</div>
weread.json 的內容是這樣:
{"data": [{ "updated": [ { "hello":"world" } ] }]}
</div>
修改后的數據就會就成這樣:
{"data": [ { "bookId":"0000001", "updated": [ { "chapterIdx": 1, "title": "序言", "hello":"world" }, { "chapterIdx": 2, "title": "第2章", "hello":"world" } ] }]}
</div>
GYHttpMock會根據 weread.json 指定的層次結構來修改原始數據,前提是 wearied.json 的數據結構需要和正常的返回數據一致,否則會導致修改失敗或者不可預知的錯誤。
實現原理
GYHttpMock的工作流程如下:

其核心實現主要包括request匹配、request攔截、response替換三個部分。
request匹配
用于判斷應用中的某個HTTP Request是否應該被mock。判斷的條件包括method、URL、Headers、Body,其中URL和Body都支持正規匹配的方式,一個httpMock可以同時匹配多個HTTP Request。
request攔截
request攔截是通過繼承 NSURLProtocol 的子類來實現。 NSURLProtocol 是iOS URL網絡加載中功能非常強大的一個類,官方文檔也有說明 NSURLProtocol ,通過重寫它的方法,可以重新定義系統網絡加載行為。在此之前,對于 NSURLConnection 的網絡請求,需要這樣注冊 NSURLProtocol 的子類 GYMockURLProtocol
[NSURLProtocol registerClass:[GYMockURLProtocol class]];
</div>
對于 NSURLSession 的網絡請求,需要替換 protocolClasses 方法
Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration"); [self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];
</div>
最后,重點是重寫 NSURLProtocol 類的 canInitWithRequest 和 startLoading 方法。 canInitWithRequest 是用于判斷是否可以發起網絡請求,可以通過這個過濾不在攔截范圍內的request,不影響App的正常網絡請求。 startLoading 是替換response數據的核心所在,成功截攔的request會進入該方法,在這個方法中替換或修改response數據,再回調給上層。
response替換
對于需要全部替換的response,實現方式是在 startLoading 方法中調中 NSURLProtocol 的 URLProtocol:didReceiveResponse:cacheStoragePolicy: 方法,將替換好的response回調給上層。對于需要部分替換的response,GYHttpMock會用NSURLConnection的方式,發起一次真正的網絡請求,待數據回來后,再與mockRequest中的response數據進行合并,最后將合并后的數據回調上層。部分替換過程中遇到兩個問題:
-
部分替換時要發出一個真實網絡請求拿到原始數據,這個請求按照之前的規則又會被NSURLProtocol截獲,從而進入死循環。解決辦法是,start request前將這個GYHttpRequest打上標記,表明是不需要再次截獲的,等拿到reponse后再將GYHttpRequest上的標記去掉,避免死循環。
-
兩個response內容合并的問題。因為json的數據結構非常靈活,可以任意層次嵌套,如何指定修改或添加某個節點下的數據是比較困難的,尤其是json中數組的嵌套,導致要指定修改數組中某個位置的元素變得非常困難。GYHttpMock采用的方式是,在mockRequest的response中指出需要修改的節點完整位置,然后用這個數據結構去匹配目標數據(具體算法請查看 GYHttpMock源碼 ,好處在于可以支持比較復雜的數據結構,但這就要求使用者對目標數據結構非常清楚。
GYHttpMock已經在 GitHub 開源,目前已用于 微信讀書 項目中,使用過程如果有問題或者建議,歡迎提交 issue 和 pull request。
</div>