Mockingjayを使ったAPI通信のユニットテスト


テストの中でもAPI通信は手動でやるには大変な領域ではないでしょうか
ケースごとにサーバの値や通信状態を変えるのはやりたくないですよね
調べてみたところ、Mockingjayというライブラリがどうも良さそうだということで、使い方をまとめてみようと思います

Mockingjayとは

https://github.com/kylef/Mockingjay
HTTP/HTTPS通信をスタブに置き換えてくれるSwiftのテスティングライブラリです。URLConnectionまたはURLSessionを使用している場合に適用できます。
私はAlamofireを使っていますが、内部的にURLSessionを使用しているため、Mockingjayによってスタブ化することができます。

Alamofire, URLSessionの通信処理をMethod Swizzlingでスタブに置き換える
こちらの記事を見るに、Method Swizzlingによってスタブ化を実現しているようです。
Method Swizzlingは既存のメソッドを別のものに置き換えてしまう強力な機能です。通信に限らずテストではとても活躍しそうですね!

使い方

Mockingjayをimportした上で、テストコード中でstubメソッドを呼び出します。シンプル!

import Mockingjay
...
stub(/* matcher(スタブ化したい通信の指定) */, /* builder(返す結果) */)

matcherbuilder

組み込みのもの

スタブ化したい通信の指定をmatcherと呼んでいます。
組み込みで3種類のmatcherが用意されています。

  • everything: すべての通信
  • uri(template): URIテンプレートを使用した指定
  • http(method, template): HTTPメソッドとURIテンプレートを使用した指定

返す結果はbuilderと呼ばれています。
組み込みで4種類のbuilderが用意されています。

  • failure(error): 通信失敗
  • http(status, headers, data): httpレスポンス
  • json(body, status, headers): シリアライズ化されたjsonデータ(String)
  • jsonData(data, status, headers): 生のjsonデータ(Data)

この2つを組み合わせて通信をスタブ化します。

// すべての通信で404を返す
stub(everything, http(status: 404))

// 指定されたURIへのPUTリクエストでbody0を返す
let body0 = [ "description": "Kyle" ]
stub(http(.put, uri: "https://github.com/kylef/Mockingjay"), json(body0))

// 指定されたURIへのGETリクエストでbody1を返す
let body1 = "{ \"parse\": \"error\" }".data(using: .utf8)!
stub(uri("https://github.com/kylef/Mockingjay"), jsonData(body1))

自作する

matcherbuilderはそれぞれ以下のように定義されています。

public typealias Matcher = (URLRequest) -> (Bool)
public typealias Builder = (URLRequest) -> (Response)

これらの定義に適合するメソッドを作成すれば、任意のmatcherbuilderを使用できます。

XCTestExpectationを使用して非同期を待つ

API通信は非同期処理のため、何も考えずにテストを書いても、通信処理が終わる前にテストが終了してしまいます。通信処理が完了するのを待つために、XCTestExpectationを使います。
私はAPI通信テスト用の便利メソッドを作成して使っています。Rxを使っていたり、クラス名が一部適当ですが、雰囲気で理解してください

API通信テスト用の便利メソッド
private func request(requestInfo: SomeInfoClass, onSuccess: @escaping (Data) -> Void, onError: @escaping (Error) -> Void) {
    let expectation = self.expectation(description: "network") // XCTestExpectation生成
    someNetworkClass.request(requestInfo).subscribe(           // 通信開始
        onSuccess: { data in
            onSuccess(data)                                    // 通信成功時に実行するテスト
            expectation.fulfill()                              // 非同期処理が完了したことを通知
    },
        onError: { error in
            onError(error)                                     // 通信失敗時に実行するテスト
            expectation.fulfill()                              // 非同期処理が完了したことを通知
    }).disposed(by: self.disposeBag)
    self.waitForExpectations(timeout: 1, handler: nil)         // 非同期処理完了通知が来るまで待つ(タイムアウト1秒)
}

テストサンプル

これまで紹介したものを組み合わせて、以下のようなテストを書いています。

テストサンプル
func testSample() {

    // スタブ化
    let body0 = [ "description": "Kyle" ]
    stub(http(.put, uri: "https://github.com/kylef/Mockingjay"), json(body0))

    // 通信処理の実行
    self.request(
        requestInfo: /* urlやパラメータなどを含んだクラス */,
        onSuccess: { data in
            // テスト実行
            let actual = /* jsonのパース */
            let expected = /* 期待するデータの作成 */
            XCTAssertEqual(actual, expected)
    },
        onError: { error in
            // このテストは通信失敗しないはずなので、入ってきたらテスト失敗するようにしておく
            XCTFail("never reach here")
    })
}

終わりに

冒頭にも少し書きましたが、通信処理のテストは書く価値がかなり高いテストだと思います
もしすべてを自力で書こうとしたら結構大変ですが、Mockingjayを利用することで割と楽にテストを書くことができます
どんどん書いていきましょう

参考

HTTPモックライブラリ「Mockingjay」を使ってみた話/swift-mockingjay
SwiftのQuickでAlamofireを使った非同期のテストを書く