Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

通信処理のテストをモックにする時に、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.requestSessionManager.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を入れ替えています。

URLSessionConfigurationprotocolClassesに独自のURLProtocolを指定することで、通信のハンドリングをカスタマイズすることができます。
MockURLProtocolはこの次に追加するクラスです。
URLProtocol.registerClassは独自のプロトコルを認識させるために必要なメソッドです。

(.mockdynamicが付いていますが、こちらは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;
}

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

最後に

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

https://github.com/tattn/Replacer

関連

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away