4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-10-07

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

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

##各クラスのロール

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

##実際のコード

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

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()
    }
}
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に準拠したモックを作成します。
モックには以下の2つの機能が必要です。

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

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にそのまま渡せます。
ここまでで、テストコードを記述するのに必要なものが揃いました。

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

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)がテスト用の準備、(4)がテスト対象の呼び出し、(5)、(6)が結果の検証です。
このように、テスト対象が依存するクラスをprotocolに置き換え、テスト用に都合の良いモックを渡せるようにするテクニックは、ユニットテストにおいてよく利用されます。

<参考書籍: iOSアプリ開発自動テストの教科書>

4
5
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
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?