通信処理のテストをモックにする時に、MockingjayというOSSを使っているのをよく見ます。
どうやって実現しているのかが気になったので、ソースを読んでみると、既存のメソッドの入れ替え (Method Swizzling)を行っていました。
意外と簡単に作れそうだったので、最小限のシンプルな実装で、URLSessionやAlamofireの通信をスタブに置き換えてみました。
ユニットテストで使えるテクニックだと思います。
例
スタブに置き換える通信の例はこちらです。
APIのサンプルとして JSON Test を利用します。
// URLSessionの場合
let url = URL(string: "http://echo.jsontest.com/key/value/one/two")!
URLSession.shared.dataTask(with: url) { data, response, error in
let json = try! JSONSerialization.jsonObject(with: data!, options: .allowFragments)
print(json)
}.resume()
// Alamofireの場合
Alamofire.request("http://echo.jsontest.com/key/value/one/two").responseJSON { response in
print(response.result.value!)
}
// 別の書き方でもOK
SessionManager.default.request("http://echo.jsontest.com/key/value/one/two").responseJSON { response in
print(response.result.value!)
}
実行するとこのような出力になります。
{
key = value;
one = two;
}
Method SwizzlingによるURLSessionConfiguration.default
の入れ替え
今回はURLSessionConfiguration.default
をモック用に置き換えるという方法で実装してみます。
Alamofire.request
やSessionManager.default
も内部で、URLSessionConfiguration.default
を使用しています。
URLSessionConfigurationにextensionで入れ替え準備用のメソッドと入れ替えるプロパティを追加します。
public extension URLSessionConfiguration {
// .defaultをモック用と入れ替えるメソッド
public class func setupMockDefaultSessionConfiguration() {
let defaultSessionConfiguration = class_getClassMethod(URLSessionConfiguration.self, #selector(getter: URLSessionConfiguration.default))
let swizzledDefaultSessionConfiguration = class_getClassMethod(URLSessionConfiguration.self, #selector(getter: URLSessionConfiguration.mock))
method_exchangeImplementations(defaultSessionConfiguration, swizzledDefaultSessionConfiguration)
}
// .defaultと入れ替えるプロパティ変数
private dynamic class var mock: URLSessionConfiguration {
let configuration = self.mock
configuration.protocolClasses?.insert(MockURLProtocol.self, at: 0)
URLProtocol.registerClass(MockURLProtocol.self)
return configuration
}
}
method_exchangeImplementations
というAPIを使用して.default
と.mock
を入れ替えています。
URLSessionConfiguration
のprotocolClasses
に独自のURLProtocolを指定することで、通信のハンドリングをカスタマイズすることができます。
MockURLProtocolはこの次に追加するクラスです。
URLProtocol.registerClass
は独自のプロトコルを認識させるために必要なメソッドです。
(.mock
にdynamic
が付いていますが、こちらはprivateなメソッドをSelector型として取り出せるようにするためです。)
モック用のURLProtocolの定義
実装の入れ替えは上記で完成したので、次は入れ替える中身を作ります。
public class MockURLProtocol: URLProtocol {
// 引数のURLRequestを処理できる場合はtrue
override open class func canInit(with request:URLRequest) -> Bool {
return true
}
// URLRequestの修正が必要でなければそのまま返す。
override open class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
// 通信開始時に呼ばれるメソッド、ここに通信のモックを実装します。
override open func startLoading() {
let delay: Double = 1.0 // 通信に1秒かかるモック
DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
self.client?.urlProtocol(self, didLoad: response!) // 結果を返す
self.client?.urlProtocolDidFinishLoading(self) // 通信が終了したことを伝える
// エラー時のハンドリングもこちらで可能です。
// self.client?.urlProtocol(self, didFailWithError: error)
}
}
// 通信停止時に呼ばれるメソッド
override open func stopLoading() {
}
private var response: Data? {
// URLなどでパターンマッチングすることで結果を切り替えることも出来る
// self.request.url
let json = "{\"mock\": \"data\"}" // モック用のJSONデータ
return json.data(using: .utf8)
}
}
override
が付いているメソッドは実装が必須のメソッドです。
特に重要なのはstartLoading
メソッドで、この中で、
client?.urlProtocol(self, didLoad: 返却するData)
client?.urlProtocolDidFinishLoading(self)
を呼び出すことで、独自のレスポンスを返すことができます。
(clientはURLProtocolが持っているURLProtocolClientというプロトコルです)
この例では決め打ちのJSONデータですが、URLProtocolはURLRequest(self.request
)を持っているので、パターンマッチングでURLやパラメータを考慮して、JSONを出し分けることも簡単にできます。
完成
以上で完成です。
最初の例を実行してみると
// スタブに置き換え
URLSessionConfiguration. setupMockDefaultSessionConfiguration()
// Alamofireの場合
Alamofire.request("http://echo.jsontest.com/key/value/one/two").responseJSON { response in
print(response.result.value!)
}
↓
{
mock = data;
}
このようにモックデータに差し替えができます。
最後に
このサンプルを使いやすくしたものをライブラリ化してみました。
気になった方はぜひ見てみてください!
関連
Using NSURLProtocol for Testing
https://yahooeng.tumblr.com/post/141143817861/using-nsurlprotocol-for-testing