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

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

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

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

# 使い方

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

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

## `matcher`と`builder`

### 組み込みのもの

スタブ化したい通信の指定を`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`)

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

```swift:例
// すべての通信で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))
```

### 自作する

`matcher`と`builder`はそれぞれ以下のように定義されています。

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

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

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

```swift: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秒)
}
```

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

```swift:テストサンプル
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](https://speakerdeck.com/yusukehosonuma/swift-mockingjay?slide=20)
[SwiftのQuickでAlamofireを使った非同期のテストを書く](https://qiita.com/kaibadash@github/items/75a8b8eea192f7191ff4)