非同期処理のユニットテスト (Spy）

# はじめに
非同期処理のユニットテストの流れを整理しました。
今回は、APIでサーバーに通信し、その結果を通知するクラスを例に説明します。

## 1. 基本情報
非同期テスト(API)に必要なクラスおよび、メソッドは下記のとおりです。

| クラス名 | メソッド名 | 説明 |
|---|---|---|
| XCTestCase | expectation |テスト対象クラスに完了通知してもらうため、XCTestExpectationのインスタンスを取得メソッドです。|
| XCTestCase | waitForExpectations |完了通知を待つためのメソッドです。<br>所定時間以内に通知がなければテスト失敗になります。|
| XCTestExpectation | fulfill |テスト対象クラスに完了通知をするメソッドです。|

# 2. テスト対象とテストクラスの関係性
##2.1. イメージ図
テスト対象クラスからの通知をモッククラスを経由し、テストクラスに通知するイメージです。

<img width="614" alt="スクリーンショット 2017-03-06 12.24.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/71694/f790732b-7a1e-2906-8f20-a16f692497e8.png">

##2.2. 今回ご説明するクラスの関係性
今回ご説明するクラスの関係性は、下記のとおりです。

|分類 | クラス名 |
|---|---|---|
|テスト対象クラス | BooksBookSearchAPI |
|モック(SPY)クラス | SpyBooksBookSearchAPILoadable |
|テストクラス | BooksBookSearchAPITests |

<img width="592" alt="スクリーンショット 2017-03-06 13.31.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/71694/011a9b0b-a282-3be8-ee6a-07c44112a4c9.png">

①BooksBookSearchAPIクラスからSpyBooksBookSearchAPILoadableクラスへの通知は、
デリゲード(setResult)で通知します。
②SpyBooksBookSearchAPILoadableクラスからBooksBookSearchAPITestsクラスへの通知は、
fulfillで通知します。

# 3. サンプル
## 3.1. テスト対象クラス
本を検索し、その結果を通知するAPIのラッパークラスです。
（[楽天ブック総合検索API](https://webservice.rakuten.co.jp/api/bookstotalsearch/))
本クラスの詳細は、割愛します。
完了通知を「setResult」をコールして、APIの呼ぶ元のクラスに通知します。

```swift:BooksBookSearchAPI.swift
import ObjectMapper

final class BooksBookSearchAPI {
    
    var loadable: BooksBookSearchLoadable?
    
    func fetch(title: String, page: Int) {
        
        let router = Router.BooksBookSearch(
            BooksBookSearchParamsBuilder.create(title: title, page: page)
        )
        
        APIClient().request(router: router) { [weak self] (response) in
            
            switch response {
            case .success(let result) :
                
                if let searchResult = Mapper<BooksBookSearchResults>().map(JSONObject: result) {
                    
                    if let result = self?.setResult(searchResult: searchResult) {
                        self?.loadable?.setResult(result: result)
                    }
                }
                
            case .failure(let error):
                self?.loadable?.setResult(result: .error(error))
            }
        }
    }
        
    private func setResult(searchResult: BooksBookSearchResults) -> BooksBookSearchStatus {
        return searchResult.items.count > 0 ? .normal(searchResult) : .noData
    }
}
```
ちなみに、OSSは、AlamofireとObjectMapperを利用しています。


### 関連クラス
```swift:BooksBookSearchStatus.swift
import Foundation

enum BooksBookSearchStatus {
    case none
    case normal(BooksBookSearchResults)
    case noData
    case offline
    case error(Error)
}
```

```swift:BooksBookSearchLoadable.swift
import Foundation

protocol BooksBookSearchLoadable {
    func setResult(result: BooksBookSearchStatus)
}
```

## 3.2. モック(SPY)クラス
プロトコルを実装したモック(SPY)クラスです。
今回はBooksBookSearchLoadableプロトコルを実装した
SpyBooksBookSearchAPILoadableクラスとしました。

※実際にAPIをコールして、その結果をテストクラスに通知しています。

### 3.2.1. 役割
①処理結果を設定する
②テストクラスへ完了通知する

### 3.2.2. サンプルコード
```swift:SpyBooksBookSearchAPILoadable.swift
import XCTest
@testable import ios_rakuten_api_demo

final class SpyBooksBookSearchAPILoadable: BooksBookSearchLoadable {
    
    var result: BooksBookSearchStatus?
    var asyncExpectation: XCTestExpectation?
    
    func setResult(result: BooksBookSearchStatus) {
        
        guard let expectation = asyncExpectation else {
            XCTFail("MocBooksBookSearchAPIDelegateが正しく設定されていない")
            return
        }
        
        self.result = result
        expectation.fulfill()
    }
}
```

##3.3. テストクラス
### 3.3.1. 役割
①テスト対象クラスのインスタンス化を行う。(BooksBookSearchAPI)
②プロトコルを実装したモッククラスのインスタンス化を行う。（SpyBooksBookSearchAPILoadable）
③テスト対象のクラスに②で取得したインスタンスを設定する。
④APIをコールする。(BooksBookSearchAPIのfetchメソッドを呼ぶ)
⑤一定時間待つ。(監視する）
⑥結果が正しいか確認する。

### 3.3.2.サンプルコード

```swift:
import XCTest
@testable import ios_rakuten_api_demo

class BooksBookSearchAPITests: XCTestCase {
    
    let api = BooksBookSearchAPI()
    let loadable = SpyBooksBookSearchAPILoadable()
    
    override func setUp() {
        super.setUp()
        api.loadable = loadable
    }
    
    override func tearDown() {
        super.tearDown()
        api.loadable = nil
    }
    
    func testSearchKeywordSwift() {
        
        let exp = expectation(description: "「Swift」で検索したときのテスト")
        loadable.asyncExpectation = exp
        
        api.fetch(title: "Swift", page: 1)
        
        waitForExpectations(timeout: 1) { error in
            if let error = error {
                XCTFail("waitForExpectationsエラー: \(error)")
            }
            
            if let result = self.loadable.result {
             
                switch result {
                case .normal(let result) :
                    XCTAssertNotNil(result)
                    XCTAssertTrue(result.items.count > 0)
                    XCTAssertEqual(result.page, 1)
                    
                case .error(let error) :
                    XCTFail(error.localizedDescription)
                    
                default:
                    XCTFail("バグです")
                }
            }
        }
    }
}
```

# まとめ
自分で作成したAPIと通信するクラスを実際に通信させて、
ユニットテストするサンプルをご紹介しました。

APIだけでなく、Delegationを利用しているクラスは、
同様にユニットテストできます。