🤖

テストをもっとラクに書けるライブラリの紹介と、async/awaitを使った非同期関数のテストの書き方

2022/12/18に公開

XCTestと組み合わせてもっとラクにテストを書くためにおすすめのライブラリ(StubKit,Nimble)を2つ紹介します。
また、async/awaitの導入により、async/awaitで書かれた非同期関数の場合は、ライブラリを使わなくても非同期のテストが簡単に書けるようになりました。そちらについても最後に触れたいとおもいます。

「これからテストを書いてみようと思うよ。書いてみたよ。もっとラクに書けない?」と思い始めた方の参考になればうれしいです:)

モックデータをラクに用意しよう

何らかのAPI通信を行なっている画面のテストを書く時に、仮データを用意するのが面倒くさい!というのがあると思います:(

たとえば、サッカー日本代表一覧をcollectionViewで表示する画面に対してテストを書くとします。
選手の情報はAPI通信で取得し、レスポンスの型を下記のようにアプリ側で作成しているはずです。

struct PlayerDetail: Codable {
    var playerName: String
    var imageURL: String
    var number: Int
    var bloodType: BloodType
    var birthday: String
    var bodyHeight: String
    var bodyWeight: String

    enum BloodType: Codable {
        case a, b, o, ab
    }
}

ViewModelやPresenterのテストとして、選手情報を取得して正しくデータがセットされているかを確認したいとき、モックデータを下記のようにベタ書きすることはありませんか?

   private let playerDetail1 = PlayersDetail.init(
        playerName: "伊東純也",
        number: 14,
        bloodType: PlayerDetail.BloodType.a,
        birthday: "1993.3.9",
        bodyHeight: "176cm",
        bodyWeight: "66kg")
    
    private let playerDetail2 = PlayersDetail.init(
        playerName: "三笘薫",
        number: 9,
        bloodType: PlayerDetail.BloodType.o,
        birthday: "1997.5.20",
        bodyHeight: "178cm",
        bodyWeight: "73kg")
    
    private let playerDetail3 = PlayersDetail.init(
        :
        :
    )

そんなとき、StubKitを使いましょう!

StubKitとは

Decodableに準拠している型であれば、その型のモックデータを自動で生成してくれるライブラリです。

https://github.com/kateinoigakukun/StubKit

StubKitを使えば、こんな感じで書くだけで、欲しい型の適当なデータが生成されます✌️
生成したデータをたとえばlet responseとかで受け取り、viewModelに注入するなりなんなり好きに使ってください:)

    var stub = Stub(type: [PlayerDetail].self)
    stub.maxSequenceLength = 3 // maxSequenceLengthは、コンテンツをいくつ作成するか指定できる
    let response = try! stub.make()

コンテンツ数の指定はオプショナルです。指定しなければデフォルトで60個作成されます。
作成量が多いほど時間がかかってしまうので、テストに最低限必要な数だけ指定したほうがよさそうです。

特定のプロパティだけ自分で任意の値をセットする、というのもできます。
色々とカスタムできそうだったので、詳しくはREADMEを見てもらえればとおもいます!

非同期処理テストを簡単に書こう

非同期処理を伴うテストをする時に、時間がかかりタイムアウトで落ちてしまうことがあるかと思います。
「0.1秒でダメなら0.2秒、それでもダメなら0.3秒...」とテストが通るタイムアウト時間を探ることはありませんか?
そんなときにNimbleを使いましょう!

Nimbleとは

非同期で更新される値に対してのテストをたった1行で書けます。
toEventuallyをつけるだけで、非同期で更新される値が成功するまで繰り返しテストを続けます。なので、タイムアウトを指定しない限りタイムアウトで落ちることはなくなります。

https://github.com/Quick/Nimble

たとえば、コンテンツのデータを非同期で取得し、画面への表示をテストしたいとします。

☟MockViewModelを用意します

MockViewModelTest.swift
class MockViewModel {
    let mockAPIClient = MockAPIClient()
    var contents: [String] = []
    func fetch() {
        mockAPIClient.getPlayers() { [weak self] result in
            guard let self = self else { return }
            switch result {
            case .success(let players):
	        // class MockAPIClientを作るなどして、getPlayers()は["伊東純也", "三笘薫", "堂安律"]返すようにしておきます
                self.contents = players
            case .failure:
                break
            }
        }
    }
}

☟普通に書けばこんな感じになるかとおもいます

MockViewModelTest.swift
class MockViewModelTest: XCTestCase {
    func testAPIリクエストをして表示() {
        let viewModel = MockViewModel()

        let exp = expectation(description: "コンテンツのロードを待つ")
        viewModel.fetch {
            exp.fulfill()
        }

        wait(for: [exp], timeout: 0.05)
        XCTAssertEqual(viewModel.contents.isEmpty, false)
        XCTAssertEqual(viewModel.contents[0], "伊東純也")
    }
}

☟Nimbleを使えば、こんな感じで書けます。

MockViewModelTest.swift
    func testAPIリクエストをして表示() {
        let viewModel = MockViewModel()

        viewModel.fetch(completion: {})
        expect(!viewModel.contents.isEmpty).toEventually(beTrue()) // Nimbleの文法
        XCTAssertEqual(viewModel.contents[0], "伊東純也") // XCTestの文法
    }

XCTestの文法で書いているコードは、Nimbleの文法で書いても全然良いです。
Nimbleの文法で書くなら、下記になります。

expect(viewModel.contents[0]).to(equal("伊東純也"))

ただ、isEmptyチェックの時点で、非同期処理が終わっていることを保証しているので、それ以降は標準のXCTestで書くというハイブリッドで良いかと思います!

beTrue()だけでなくequalを使ってコンテンツの値が適切か確認することも可能です。

expect(viewModel.contents[0]).toEventually(equal("伊東純也"))

(Nimbleの文法についてはぜひREADMEを見てみてください🌟)



☟Nimble使用についてのメリット・デメリットがまとめられている記事です。非常に参考になるかと思います。

https://web-y.dev/2021/02/01/ios-unittest-nimble/

async/awaitを使った非同期関数のテストの書き方

async/awaitを使って非同期処理を書いている場合は、ライブラリを使わなくても、expectationを書かずに非同期関数のテストが書けるようになりました。

テストメソッドに async throws を付けるだけです。

たとえば、こんな感じです。

class MockViewModel {
    let mockAPIClient = MockAPIClient()
    var contents: [String] = []
    func fetch() async {
        Task {
            do {
                // class MockAPIClientを作るなどして、getPlayers()は["伊東純也", "三笘薫", "堂安律"]返すようにしておきます
                let response = try await mockAPIClient.getPlayers()
                await setContents(contents: response)
            } catch {
                // エラー処理
            }
        }
    }

    @MainActor private func setContents(contents: [String]) {
        contents = contents
    }
}

class MockViewModelTest: XCTestCase {
    func testAPIリクエストをして表示() async throws {
        let viewModel = MockViewModel()
        
        await viewModel.fetch()
        
        XCTAssertEqual(viewModel.contents.isEmpty, false)
        XCTAssertEqual(viewModel.contents[0], "伊東純也")
    }
}

ライブラリ依存を増やさないためにも、非同期関数はなるべくasync/awaitを使って書いていくのが良いとおもいました!
以上、参考になる部分があればうれしいです!😈

Discussion