LoginSignup
19
12

More than 5 years have passed since last update.

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

Posted at

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

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を使っていたり、クラス名が一部適当ですが、雰囲気で理解してください:sweat_smile:

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")
    })
}

終わりに

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

参考

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

19
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
12