【Swift】Combineで作るアプリのテストコードを書いてみた

## はじめに
`Combine`を使った通信まわりのテストコードの情報がまだ少ないように感じましたので、作ったサンプルアプリのコードを載せてみようと思い、書いてみました。
今回のアプリのアーキテクチャは、MVVMです。

## この記事でわかること
- `Combine`で`URLSession`を使ったリクエストを書いたときのテストコードの書き方
- MVVMで実装したときのテストコード

Modelのテストよりも、個人的にはViewModelのテストのほうが、`Combine`特有の実装が必要だと思うので、ViewModelの方が重要かなと思います。

## Model層のテスト
今回のModel層は、実際にAPIに対してリクエストするところが該当します。
コードには、すべて`Combine`をimportしてください。

#### アプリのコード
```swift
protocol GithubAPIClientProtocl: AnyObject {
    func searchRepositories(searchWord: String) -> AnyPublisher<GithubRepositryModel, Error>
}

final class GithubAPIClient: GithubAPIClientProtocl {
    static let shared = GithubAPIClient()
    private init() {}
    
    func searchRepositories(searchWord: String) -> AnyPublisher<GithubRepositryModel, Error> {
        let url = URL(string: "https://api.github.com/search/repositories?q=\(searchWord)&per_page=20")!
        
        return URLSession.shared.dataTaskPublisher(for: url)
            .map { $0.data }
            .decode(type: GithubRepositryModel.self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
}
```

`Combine`で`URLSession`を使うときの一般的な書き方と同じだと思います。

`GithubAPIClientProtocl`は、あとでViewModelのテストをする時に使いますが、一旦関係ありません。

アプリ側で使うデータモデルについては、下記のAPI仕様書をご参考にしてください。
https://docs.github.com/ja/rest/reference/search

#### テストコード
```swift

var cancellable = Set<AnyCancellable>()

func testGithubAPIClientSearch() throws {
    let expectation = expectation(description: "testGithubAPIClientSearch")
    GithubAPIClient.shared.searchRepositories(searchWord: "Swift")
        .sink { completion in
            switch completion {
            case .finished:
                break
            case .failure(let e):
                print(e.localizedDescription)
                XCTFail()
            }
        } receiveValue: { list in
            print(list)
            XCTAssert(true)
            expectation.fulfill()
        }
        .store(in: &cancellable)
    wait(for: [expectation], timeout: 10)
}
```

Model層のテストの場合、普通にModelのリクエストを読んで、それを`.sink`（購読）するだけなので、シンプルだと思います。

## ViewModel層のテスト
#### アプリのコード
```swift
class SearchGithubRepositoriesViewModel: ObservableObject {
    private let githubApiClient: GithubAPIClientProtocl
    private var cancellables = Set<AnyCancellable>()
    
    @Published var repositories = [GithubRepositryModel.Item]()
    
    init(githubApiClient: GithubAPIClientProtocl = GithubAPIClient.shared) {
        self.githubApiClient = githubApiClient
    }
    
    func searchButtonTapped(searchWord: String) {
        githubApiClient
            .searchRepositories(searchWord: searchWord)
            .sink { completion in
                switch completion {
                case .finished:
                    break
                case .failure(_):
                    break
                }
            } receiveValue: { model in
                self.repositories = model.items
            }
            .store(in: &cancellables)
    }
}
```

`searchButtonTapped(searchWord: String)`が画面側の検索ボタンが押されたら呼び出されるそうていです。今回は、このメソッドに対するテストコードを書いてみました。

#### テストコード
```swift
class GitHubAPIClientMock: GithubAPIClientProtocl {
    let expectedItems: [GithubRepositryModel.Item] = (1...2).map {
        .init(id: $0, fullName: "\($0) FullName", htmlURL: "", stargazersCount: $0, forksCount: $0, watchersCount: $0, description: "Description \($0)")
    }
    
    func searchRepositories(searchWord: String) -> AnyPublisher<GithubRepositryModel, Error> {
        Just(GithubRepositryModel(totalCount: 10, incompleteResults: false, items: expectedItems))
            .setFailureType(to: Error.self)
            .eraseToAnyPublisher()
    }
}

class SearchGithubRepositoriesTest: XCTestCase {
    var cancellable = Set<AnyCancellable>()

    func testGitHubSearchViewModel() {
        let mockModel = GitHubAPIClientMock()
        let viewModel = SearchGithubRepositoriesViewModel(githubApiClient: mockModel)
        viewModel.searchButtonTapped(searchWord: "")
        
        XCTAssertEqual(mockModel.expectedItems, viewModel.repositories)
    }
}
```

まず、最初のModel側のコードに記載した、`GithubAPIClientProtocl`を継承した、Mockを作ります。
これで、実際にAPIにリクエストしなくても、テストケースで使いたい仮データをコードで生成することができるようになります。
= `単体テスト`

**ポイントは、2つあります。**

1つ目は、
`Just`を使って瞬時に結果を返すように制御するところです。
このように書けば、APIから値が返されたことを仮定してテストすることができます。

**`Just`について**

https://developer.apple.com/documentation/combine/just

2つ目は、
`.setFailureType(to: Error.self)`のところです。
`AnyPublisher<GithubRepositryModel, Error>`型で返すためには、何らかのエラーを想定させなければなりません。

`.setFailureType(to: Error.self)`を書かないと、戻り値が`AnyPublisher<GithubRepositryModel, Never>`になってしまい、コンパイルエラーになります。

**`setFailureType(to:)`について**

https://developer.apple.com/documentation/combine/fail/setfailuretype(to:)


今回は以上になります。