Upgrade to Pro — share decks privately, control downloads, hide ads and more …

通信のスタブ化を自作した話 (iOS)

通信のスタブ化を自作した話 (iOS)

iOSでのユニットテスト用に通信をスタブ化する方法とその実装方法について。

@iOS_LT #25

Tatsuya Tanaka

January 14, 2017
Tweet

More Decks by Tatsuya Tanaka

Other Decks in Technology

Transcript

  1. 通信の スタブ化 を自作した話
    Presented by Tatsuya Tanaka
    tattn tattn tanakasan2525

    View Slide

  2. 通信部分のユニットテスト
    実際に通信をしてテストすることも可能。
    しかし、
    スタブ化することで以下のような メリット がある。
    無駄なサー
    バー
    負荷を減らせる
    サー
    バー
    が開発中・
    停止中でもテストできる
    任意の通信エラー
    をテストできる
    2

    View Slide

  3. 通信部分のスタブ化
    iOS
    では3
    つの方法がよく用いられている。
    1.
    依存性注入 (DI)
    2.
    継承
    3.
    ライブラリ (OHHTTPStubs, OCMock, Mockingjay)
    3

    View Slide

  4. 1.
    依存性注入 (DI)
    class APIClient {
    let requester: Requestable
    init(requester: Requestable = RealRequester()) {
    self.requester = requester
    }
    ...
    }
    class RealRequester: Requestable {} //
    実際の通信用
    class MockRequester: Requestable {} //
    モック用
    APIClient(requester: MockRequester()) //
    モックに差し替え
    予めDI
    を意識した設計が必要
    4

    View Slide

  5. 2.
    継承
    open class APIClient {
    open func request() {}
    }
    class MockAPIClient: APIClient {
    override func request() {} //
    モックに上書き
    }
    let apiClient: APIClient = MockAPIClient()
    継承できないstruct
    では不可
    クラスごとにモック用のクラスを作るのが面倒
    5

    View Slide

  6. 3.
    ライブラリ
    (OHHTTPStubs, OCMock, Mockingjay)
    OHHTTPStubs
    で、
    レスポンスをJSON
    に差し替える場合
    stub(isHost("mywebservice.com")) { _ in
    let stubPath = OHPathForFile("wsresponse.json", type(of: self))
    return fixture(filePath: stubPath!,
    headers: ["Content-Type":"application/json"])
    }
    使いやすい
    6

    View Slide

  7. 今回は3
    のように扱える仕組みを実装
    7

    View Slide

  8. その方法とは...
    8

    View Slide

  9. Method Swizzling
    &
    URLProtocol
    9

    View Slide

  10. Method Swizzling
    メソッドの実装を差し替える手法。
    Objective-C
    のランタイムを使用する。
    紹介時間が足りないので
    詳しくはアヒルの方の記事を見てね
    http://qiita.com/paming/items/25eaf89e4f448ab05752
    注意: Pure
    なSwift
    のクラスでは利用できない
    (Swift
    のリフレクションは読み込み専用のため)
    10

    View Slide

  11. URLProtocol
    サポー
    ト外のプロトコルで通信する場合や
    リクエストを独自の方法で処理する時に使うクラス。
    継承して独自の処理を実装。
    少なくとも4
    つのメソッドをoverride
    が必要。
    class func canInit(with request: URLRequest) -> Bool
    class func canonicalRequest(for request: URLRequest) -> URLRequest
    func startLoading()
    func stopLoading()
    11

    View Slide

  12. class func canInit(with request: URLRequest) ->
    Bool
    引数のリクエストが処理できるか否かで真偽値を返す
    func startLoading()
    独自のリクエストの処理を実装する
    func stopLoading()
    リクエストのキャンセル時の処理を実装する
    12

    View Slide

  13. スタブ化できそうな気が
    してきますね
    13

    View Slide

  14. モック用URLProtocol
    の実装
    public class MockURLProtocol: URLProtocol {
    override open class func canInit(with request: URLRequest) -> Bool {
    return true
    }
    override open class func canonicalRequest(for request: URLRequest)
    -> URLRequest {
    return request
    }
    override open func startLoading() {
    //
    次のスライドで実装
    }
    override open func stopLoading() {
    }
    } 14

    View Slide

  15. モックデー
    タを返す
    override open func startLoading() {
    let delay: Double = 1.0 //
    通信に1
    秒かかるモック
    DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
    let json: [String: Any] = ["mock": "data"]
    let data = try! JSONSerialization.data(withJSONObject: json,
    options: .prettyPrinted)
    self.client?.urlProtocol(self, didLoad: data!)
    self.client?.urlProtocolDidFinishLoading(self)
    }
    }
    client
    はURLProtocol
    が持っているURLProtocolClient
    プロトコル。
    これを利用して結果を返す。
    15

    View Slide

  16. 作ったプロトコルを使ってみる
    プロトコルを登録
    URLProtocol.registerClass(MockURLProtocol.self)
    URLSessionCon guration
    にもプロトコルを登録
    let configuration = URLSessionConfiguration.default
    configuration.protocolClasses?.insert(MockURLProtocol.self, at: 0)
    16

    View Slide

  17. 作ったプロトコルを使ってみる
    作ったcon guration
    を利用して通信
    let url = URL(string: "
    テストするAPI
    のURL")!
    URLSession(configuration: configuration).dataTask(with: url) { data, _, _ in
    let json = try! JSONSerialization.jsonObject(with: data!,
    options: .allowFragments)
    print(json)
    }.resume()
    json
    にはモックデー
    タの["mock": "data"]
    が入っている。
    (
    通信はしていないので"
    テストするAPI
    のURL"
    のまま動かしてもこの結果)
    17

    View Slide

  18. 18

    View Slide

  19. ですが
    19

    View Slide

  20. 独自のプロトコルの作成
    public class MockURLProtocol: URLProtocol { ... }
    プロトコルを登録
    URLProtocol.registerClass(MockURLProtocol.self)
    URLSessionCon guration
    にプロトコルを登録
    let configuration = URLSessionConfiguration.default
    configuration.protocolClasses?.insert(MockURLProtocol.self, at: 0)
    con guration
    をURLSession
    に渡す
    URLSession(configuration: configuration) 20

    View Slide

  21. 面倒
    21

    View Slide

  22. そこで Method Swizzling
    22

    View Slide

  23. URLSessionCon guration
    の実装を置換
    public extension URLSessionConfiguration {
    public class func setupMock() {
    let `default` = class_getClassMethod(URLSessionConfiguration.self,
    #selector(getter: URLSessionConfiguration.default))
    let swizzled = class_getClassMethod(URLSessionConfiguration.self,
    #selector(getter: URLSessionConfiguration.mock))
    method_exchangeImplementations(`default`, swizzled)
    }
    private dynamic class var mock: URLSessionConfiguration {
    let configuration = self.mock
    configuration.protocolClasses?.insert(MockURLProtocol.self, at: 0)
    URLProtocol.registerClass(MockURLProtocol.self)
    return configuration
    }
    }
    23

    View Slide

  24. すると、
    このようになります
    24

    View Slide

  25. //
    モックに置き換え
    URLSessionConfiguration.setupMock()
    let url = URL(string: "
    テストするAPI
    のURL")!
    URLSession.shared.dataTask(with: url) { data, _, _ in
    let json = try! JSONSerialization.jsonObject(with: data!,
    options: .allowFragments)
    print(json)
    }.resume()
    It's
    シンプル
    ( URLSession.shared
    でも使えます)
    25

    View Slide

  26. もっと使いやすくしたい
    26

    View Slide

  27. ライブラリ化しました (
    宣伝)
    https://github.com/tattn/Mokei
    Method Chain
    でレスポンスを自由にカスタマイズ
    stub.url("example.com")
    .json(["test": "data"])
    .delay(0.5) // wait for 500ms
    .statusCode(400)
    Bundle
    内のJSON
    ファイルをスタブとして登録可能
    stub.json(filename: "fixture") // fixture.json
    Quick/Alamo re
    でも使えます 27

    View Slide

  28. 車輪の再発明楽しい
    28

    View Slide

  29. 参考
    Alamo re, URLSession
    の通信処理をMethod Swizzling
    でスタブに置き
    換える
    http://qiita.com/tattn/items/e7db12f84fa51b3631d2
    Swift
    における現実的なモック
    https://realm.io/jp/news/tryswift-veronica-ray-real-world-mocking-
    swift/
    Using NSURLProtocol for Testing
    https://yahooeng.tumblr.com/post/141143817861/using-
    nsurlprotocol-for-testing
    29

    View Slide