Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

その方法とは... 8

Slide 9

Slide 9 text

Method Swizzling & URLProtocol 9

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

モック用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

Slide 15

Slide 15 text

モックデー タを返す 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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

作ったプロトコルを使ってみる 作った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

Slide 18

Slide 18 text

18

Slide 19

Slide 19 text

ですが 19

Slide 20

Slide 20 text

独自のプロトコルの作成 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

Slide 21

Slide 21 text

面倒 21

Slide 22

Slide 22 text

そこで Method Swizzling 22

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

// モックに置き換え 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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

ライブラリ化しました ( 宣伝) 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

Slide 28

Slide 28 text

車輪の再発明楽しい 28

Slide 29

Slide 29 text

参考 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