6
2

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 1 year has passed since last update.

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

Posted at

はじめに

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

この記事でわかること

  • CombineURLSessionを使ったリクエストを書いたときのテストコードの書き方
  • MVVMで実装したときのテストコード

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

Model層のテスト

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

アプリのコード

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

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

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

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

テストコード


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層のテスト

アプリのコード

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)が画面側の検索ボタンが押されたら呼び出されるそうていです。今回は、このメソッドに対するテストコードを書いてみました。

テストコード

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について

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

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

setFailureType(to:)について

今回は以上になります。

6
2
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
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?