【Swift】Quick + OHHTTPStubs で API のテストを書いていく


Classi Advent Calendar 2016 1 日目です。
現在開発中の iOS アプリで Quick を使っており、そこで実施している API のテストについて書いていきます。

Quick とは

Xcode でプロジェクトを作ると XCTest がデフォルトのテストフレームワークとなっています。
ですが、 Quick を使うと rspec のように書けるため好んで利用しています。

XCTest:

class DateExtensionXCTest: XCTestCase {
    let date = Date(timeIntervalSince1970: TimeInterval(0))
    func yyyy年MM月dd日_yyyy年MM月dd日のフォーマットになっていること() {
        XCTAssertEqual(date.yyyy年MM月dd日, "1970年01月01日")
    }
}

Quick:

class DateExtensionSpec: QuickSpec {
    override func spec() {
        let date = Date(timeIntervalSince1970: TimeInterval(0))
        describe("yyyy年MM月dd日") {
            it("yyyy年MM月dd日 のフォーマットになっていること") {
                expect(date.yyyy年MM月dd日).to(equal("1970年01月01日"))
            }
        }
    }
}

具体的な違いについては XCTestと比較しつつQuickについて説明する でわかりやすく説明されています。

API のテスト

API Spec を書いていくときにいくつか困ったことがあったので、その解決策です。

API を Stub しテストする

API のテストではあらかじめ json ファイルを用意し、レスポンスを Stub します。
API の Stub のために AliSoftware/OHHTTPStubs を利用します。

class : QuickSpec {
    override func spec() {
        describe("test api を実行") {
            var response = false

            context("API にアクセスに成功した場合") {
                beforeEach {
                    // 1. 指定の Host へのアクセスを GET_test.json に Stub する。
                    _ = stub(condition: isHost("example.com")) { _ in
                        let stubPath = OHPathForFile("GET_test.json", type(of: self))
                        return fixture(filePath: stubPath!, headers: nil)
                    }

                    // done() を実行すると処理が終わる
                    waitUntil { done in
                        // 2. API を実行する
                        Session.send(TestingRequest()) { result in
                            switch result {
                            case .success(let r): response = r
                            case .failure(_): break
                            }
                            // 3. beforeEach が完了
                            done()
                        }
                    }
                }
                afterEach {
                    OHHTTPStubs.removeAllStubs()
                }

                it("response が GET_test.json と同様に true になっていること") {
                    expect(response).to(beTrue())
                }
            }
        }
    }
}
  1. 指定の Host へのアクセスを GET_test.json に Stub しています。
    • json は {"response": true} という感じのを置いています。
  2. API を実行します。
  3. done() を呼んで処理を終了させます。
    • waitUntil { done in done() } を使い、処理が完了するのを待っています。
  4. 最後に、 it 内で response が true になっていればテストが通ります。

API を Stub しテストを実行するのはざっくり上記の流れでできます

↑のやり方だと複数エンドポイントの Stub ができなくない?

↑のやり方は host だけの判断で Stub するため、複数 API を実行する場合上記の書き方ではできません。
その場合は下記のメソッドを使います。

  • OHHTTPStubs.stubRequests(passingTest: OHHTTPStubsTestBlock, withStubResponse: OHHTTPStubsResponseBlock)

このような HttpHelper を作成すると便利です。

class HttpHelper {
    let host = "example.com"

    static func stubRequest(path: String, file: String, status: Int32 = 200, headers: [String: String]? = nil) {
        OHHTTPStubs.stubRequests(
            passingTest: { (request) -> Bool in
                // path が一致していたら Stub する
                return request.url?.path == path
            }, withStubResponse: { (request) -> OHHTTPStubsResponse in
                // Stub 用の json ファイルを取得
                let stubPath = OHPathForFile(file, type(of: self))
                return OHHTTPStubsResponse(fileAtPath: stubPath!, statusCode: status, headers: headers)
            }
        )
    }
}

上記のヘルパーを使うと...

class MultiUserRequestSpec: QuickSpec {
    override func spec() {
        describe("ログインとuser情報を取得する API を実行") {
            context("/user/1, /user/2 どちらも成功した場合") {
                var response: (userName1: String, userName2: String) = ("", "")

                beforeEach {
                    // /users/1, users/2 を Stub する
                    HttpHelper.stubRequest(path: "/users/1", file: "user1.json")
                    HttpHelper.stubRequest(path: "/users/2", file: "user2.json")

                    waitUntil { done in
                        // API を実行
                        Session.send(MultiUserRequest()) { result in
                            switch result {
                            case .success(let r): response = r
                            case .failure(_): break
                            }
                            done()
                        }
                    }
                }
                afterEach {
                    OHHTTPStubs.removeAllStubs()
                }

                it("response が user1.json, user2.json と同一である事") {
                    expect(response.userName1).to(equal("高海千歌"))
                    expect(response.userName2).to(equal("渡辺曜"))
                }
            }
        }
    }
}

それぞれのエンドポイントが Stub され、 users/1, users/2 それぞれの json を返す事が出来ました
また、処理をラップした事で 1 行で Stub することが出来ました

API の実行を失敗させたい場合は?

Response Status を status で指定する事ができます。

HttpHelper.stubRequest(path: "/users/1", file: "500.json", status: 500)

これで Response Status Code: 500 の json を返す事ができます。

最後に

Quick で API テストをする際には上記のやり方で一通りできると思います。
API を Stub できると、 サーバサイドの開発が遅れたりサーバが止まっていてもアプリ側の開発が続けられるため、とても便利です。

UI 関連のテストはなかなか骨が折れますが、 Model など書けるところはしっかりと書いていきましょう!