APIを利用するModelクラスをテストする

##前提
ここでは私自身の備忘録として本記事の執筆に至っています。
仔細な間違い等ございましたらご指摘いただけますと幸いです。

##実装すること
今回はXCTestを用いて実際のAPIを叩きそれに対するテストコードを実装してみたいと思います。
実装する内容としてはユーザー名を用いGithubのレポジトリからスター数を5以上のものを返すModelクラスをテストしてみたいと思います。

##各クラスのロール

| 　　　　クラス |　ロール　　　　　　　　　　　　　|
|:-----------|------------:|
| GitHubRepository | GitHubレポジトリを表現するEntityクラス |
| GitHubAPIClient | GitHubのAPIを叩いてデータを取得するAPIクライアント |
| GitHubRepositoryManager | 指定したユーザー名のレポジトリ一覧を取得するModelクラス |

##実際のコード



```swift:GitHubRepository
// GitHubリポジトリを表現するEntityクラス

struct GitHubRepository: Codable, Equatable {
    let id: Int
    let star: Int
    let name: String
    
    enum CodingKeys: String, CodingKey {
        case id
        case star = "stargazers_count"
        case name
    }
}

```



```swift:GitHubAPIClient
protocol GitHubAPIClientProtocol {
    func fetchRepositories(user: String, handler: @escaping([GitHubRepository]?) -> Void)
}

// GitHubのAPIを叩いてデータを取得するAPIクライアント
// 作成したプロトコルに準拠するようにする
class GitHubAPIClient: GitHubAPIClientProtocol {
    
    // ユーザー名を受け取り、そのユーザーのリポジトリ一覧を取得する
    // Parameters:
    //  - user: ユーザー名
    //  - handler: コールバック(引数にリポジトリ一覧が渡される)
    
    func fetchRepositories(user: String, handler: @escaping([GitHubRepository]?) -> Void) {
        let url = URL(string: "http://api.github.com/users/\(user)/repos")!
        let request = URLRequest(url: url)
        let task = URLSession.shared.dataTask(with: request) { data, _, error in
            guard let data = data, error == nil else {
                handler(nil)
                return
            }
            let repos = try! JSONDecoder().decode([GitHubRepository].self, from: data)
            DispatchQueue.main.async {
                handler(repos)
            }
        }
        task.resume()
    }
}
```



```swift:GitHubRepositoryManager
class GitHubRepositoryManager {
    
    // 内部で利用している型をProtocolに変更し、GitHubAPIClientという具体的な型ではなくGitHubAPIClientProtocolというインターフェースにのみ依存させた
    private let client: GitHubAPIClientProtocol
    private var repos: [GitHubRepository]?
    
    // スターが5以上のリポジトリを返す(未取得の場合は空)
    var majorRepositories: [GitHubRepository] {
        guard let repositories = self.repos else { return [] }
        return repositories.filter({ $0.star >= 5 })
    }
    
    // 内部でAPIClientを生成している
//    init() {
//        self.client = GitHubAPIClient()
//    }
    
    // 内部でGitHubAPIClientを生成せず、コンストラクタ経由で渡せるようにした
    init(client: GitHubAPIClientProtocol = GitHubAPIClient()) {
        self.client = client
    }
    
    // 指定されたユーザー名のリポジトリ一覧を読み込み、完了したらコールバックを呼び出す
    func load(user: String, completion: @escaping() -> Void) {
        self.client.fetchRepositories(user: user) { repositories in
            self.repos = repositories
            completion()
        }
    }
}
```

今回はManagerクラスのloadメソッドを呼び出した後で、majorRepositoriesプロパティが正しいリポジトリ一覧（5以上のスター数）を返すことをテストします。

###Protocolに準拠させてAPIClientを代替可能にする
今回はよりテストをしやすい環境を作ることを意識し、protocolを利用し、Manager内部でのAPIClientに対する依存関係を解消しています。
このようにすることで、ManagerクラスはAPIClientという具体的な形ではなくAPIClientProtocolというインターフェースにのみ依存することができ、よりテストしやすくなります。

##テスト用のモックを作成する
次にテスト用にAPIClientProtocolに準拠したモックを作成します。
モックには以下の２つの機能が必要です。

1.fetchRepositoriesメソッド呼び出し時の引数userを記録する
（意図した通りの引数で呼び出されているかを検証するため）
2.任意のGitHubRepositoryの配列をコールバックで返却する
(テスト用のデータとして設定した値を返却できるようにするため)

```swift:SampleTestCodeTests
// テスト用にGitHubAPIClientProtocolを準拠したモックを作成する
class MockGitHubAPIClient: GitHubAPIClientProtocol {
    
    // 返却されたリポジトリ一覧を保持
    var returnRepositories: [GitHubRepository]
    // 呼び出された引数を記録
    var argsUser: String?
    
    // コンストラクタでテスト用のデータを受け取る
    init(repositories: [GitHubRepository]) {
        self.returnRepositories = repositories
    }
    
    // 引数を記録
    func fetchRepositories(user: String, handler: @escaping ([GitHubRepository]?) -> Void) {
        self.argsUser = user
        
        handler(self.returnRepositories)
    }
}
```

ここで作成したモックはGitHubAPIClientProtocolに準拠してるのでテスト対象のGitHubRepositoryMangerにそのまま渡せます。
ここまでで、テストコードを記述するのに必要なものが揃いました。

##作成したモックを使ってテストする

```swift:SampleTestCodeTests

import XCTest
@testable import プロジェクトターゲット名

class SampleTestCodeTests: XCTestCase {

    override func setUpWithError() throws {
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }

    override func tearDownWithError() throws {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
    }
    
    func testMajorRepositories() {
        
        // テスト用のリポジトリ一覧 (1)
        let testRepositories: [GitHubRepository] = [
            GitHubRepository(id: 0, star: 4, name: ""),
            GitHubRepository(id: 1, star: 5, name: ""),
            GitHubRepository(id: 2, star: 6, name: "")
        ]
        
        // モックを作成 (2)
        let mockClient = MockGitHubAPIClient(repositories: testRepositories)
        
        // テスト対象を生成する際にモックを渡す (3)
        let manager = GitHubRepositoryManager(client: mockClient)
        
        //テスト対象のメソッドを呼び出し (4)
        manager.load(user: "apple") {
            // 引数の検証 (5)
            XCTAssertEqual(mockClient.argsUser, "apple")
            
            // 結果の検証 (6)
            XCTAssertEqual(manager.majorRepositories.count, 2)
            XCTAssertEqual(manager.majorRepositories[0].id, 1)
            XCTAssertEqual(manager.majorRepositories[1].id, 2)
        }
    }
}
```

(1)~(3)がテスト用の準備、（４）がテスト対象の呼び出し、(5)、(6)が結果の検証です。
このように、テスト対象が依存するクラスをprotocolに置き換え、テスト用に都合の良いモックを渡せるようにするテクニックは、ユニットテストにおいてよく利用されます。



＜参考書籍：　iOSアプリ開発自動テストの教科書＞