Alamofire, URLSessionの通信処理をMethod Swizzlingでスタブに置き換える

通信処理のテストをモックにする時に、[Mockingjay](https://github.com/kylef/Mockingjay)というOSSを使っているのをよく見ます。
どうやって実現しているのかが気になったので、ソースを読んでみると、既存のメソッドの入れ替え (Method Swizzling)を行っていました。

意外と簡単に作れそうだったので、最小限のシンプルな実装で、URLSessionやAlamofireの通信をスタブに置き換えてみました。

ユニットテストで使えるテクニックだと思います。

# 例

スタブに置き換える通信の例はこちらです。
APIのサンプルとして [JSON Test](http://www.jsontest.com/) を利用します。

```swift
// 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で入れ替え準備用のメソッドと入れ替えるプロパティを追加します。

```swift
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の定義

実装の入れ替えは上記で完成したので、次は入れ替える中身を作ります。

```swift
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`メソッドで、この中で、

```swift
client?.urlProtocol(self, didLoad: 返却するData)
client?.urlProtocolDidFinishLoading(self)
```

を呼び出すことで、独自のレスポンスを返すことができます。
(clientはURLProtocolが持っているURLProtocolClientというプロトコルです)


この例では決め打ちのJSONデータですが、URLProtocolはURLRequest(`self.request`)を持っているので、パターンマッチングでURLやパラメータを考慮して、JSONを出し分けることも簡単にできます。


# 完成

以上で完成です。

最初の例を実行してみると

```swift
// スタブに置き換え
URLSessionConfiguration. setupMockDefaultSessionConfiguration()

// Alamofireの場合
Alamofire.request("http://echo.jsontest.com/key/value/one/two").responseJSON { response in
    print(response.result.value!)
}
```

↓

```
{
    mock = data;
}
```

このようにモックデータに差し替えができます。


# 最後に

このサンプルを使いやすくしたものをライブラリ化してみました。
気になった方はぜひ見てみてください！

https://github.com/tattn/Replacer

# 関連

Using NSURLProtocol for Testing
https://yahooeng.tumblr.com/post/141143817861/using-nsurlprotocol-for-testing