48
30

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 5 years have passed since last update.

Cuckooを使ったiOSアプリのユニットテスト

Posted at

今までモックを自作していた勢ですが、自動生成してくれるフレームワークを見つけたので、使い方をまとめます:trophy:
ライブラリはだいぶ前から存在したらしい・・・なんで知らなかったんだろう・・・:pensive:

がんばってモック書いてた記事↓
iOSアプリのモックを使った単体テスト

Cuckooとは

https://github.com/Brightify/Cuckoo
プロダクトコードからテストに必要なモックを自動生成してくれる&テストをサポートしてくれるフレームワークです。
AndroidのMockitoから影響を受けており、テストの書き方が似ているらしいです。僕自身はMockitoの経験が無いのですが、経験者がCuckooを見て似てると言ってました。

テストまでの流れ

ざっくり書くと以下のようになります。

  1. インストール
  2. Run Scriptの設定
  3. モッククラスを含むファイルの生成とプロジェクトへの取り込み
  4. テスト記述

インストール

https://github.com/Brightify/Cuckoo#1-installation
CocoaPodsかCarthageで入れましょう。
CocoaPodsの場合は、ちゃんとユニットテストのターゲットに入れるようにしてください。プロダクトのターゲットにインストールしてしまうと、実機で起動しようとしたときにXCTestが無いと怒られて起動できなくなります。

CocoaPods
pod "Cuckoo"
Carthage
github "SwiftKit/Cuckoo"

Run Scriptの設定

ユニットテストのターゲットのBuild Phasesに以下の内容のRun Scriptを追加します。
FileName[123].swiftのところはモックを作りたいプロトコルorクラスを含むファイルを必要なだけ指定します。
これでテストのビルドが走るたびに、指定したファイルからモックを自動生成してくれます。
OUTPUT_FILEは必要があれば適宜変更してください。

RunScript(CocoaPodsでインストールした場合)
# Define output file. Change "$PROJECT_DIR/${PROJECT_NAME}Tests" to your test's root source folder, if it's not the default name.
OUTPUT_FILE="$PROJECT_DIR/${PROJECT_NAME}Tests/GeneratedMocks.swift"
echo "Generated Mocks File = $OUTPUT_FILE"

# Define input directory. Change "${PROJECT_DIR}/${PROJECT_NAME}" to your project's root source folder, if it's not the default name.
INPUT_DIR="${PROJECT_DIR}/${PROJECT_NAME}"
echo "Mocks Input Directory = $INPUT_DIR"

# Generate mock files, include as many input files as you'd like to create mocks for.
"${PODS_ROOT}/Cuckoo/run" generate --testable "$PROJECT_NAME" \
--output "${OUTPUT_FILE}" \
"$INPUT_DIR/FileName1.swift" \
"$INPUT_DIR/FileName2.swift" \
"$INPUT_DIR/FileName3.swift"
# ... and so forth, the last line should never end with a backslash

# After running once, locate `GeneratedMocks.swift` and drag it into your Xcode test target group.

Carthageでインストールした場合は"${PODS_ROOT}/Cuckoo/run""Carthage/Checkouts/Cuckoo/run"に置き換えてください。

モッククラスを含むファイルの生成とプロジェクトへの取り込み

テストを実行すると、上のRun Scriptで指定したOUTPUT_FILEにファイルが生成されるので、テストターゲット内の適当な場所に手動で取り込みます。

テスト記述

モックを使ってテストを書きましょう。テストは以下の流れになります。

  1. テスト対象クラスやモッククラスの生成
  2. スタブの生成
  3. テストメソッドの実行
  4. 結果の検証
  5. (テストデータをMatchableに適合)

テストコードはサンプルが無いとわかりにくいので、以下のクラスを例にして説明します。

テスト対象のクラスと依存しているプロトコル
// テスト対象クラスが適合するプロトコル
protocol HogePresenter: class {
    weak var view: HogeView? { get set }
    var getDataUseCase: HogeUseCase! { get set }
    
    func requestData()
}

// テスト対象クラス
class HogePresenterImpl: HogePresenter {
    weak var view: HogeView?
    var getDataUseCase: HogeUseCase!
    
    // APIとかでデータ取ってきて画面を更新するイメージ
    func requestData() {
        let data = self.getDataUseCase.execute()
        self.view?.showData(data)
    }
}

// 依存プロトコル1
protocol HogeView: class {
    func showData(_ data: SomeData) // 画面を更新する
}

// 依存プロトコル2
protocol HogeUseCase: class {
    func execute() -> SomeData // APIとかでデータ取ってくる
}

テスト対象クラスやモッククラスの生成

モッククラスは実際のクラス/プロトコル名の頭にMockをつけたものが生成されているので、それを使います。
テストケース毎に必要になるので、setUp()で作るのが良いかと思います。

テスト対象クラスやモッククラスの生成
import XCTest
import Cuckoo
@testable import [プロダクトターゲット]

class HogePresenterTests: XCTestCase {
    // テスト対象クラスやモックはプロパティとして持っておきます
    var presenter: HogePresenter!
    var mockView: MockHogeView!
    var mockUseCase: MockHogeUseCase!

    override func setUp() {
        // 必要なクラスの生成
        self.presenter = HogePresenterImpl()
        self.mockView = MockHogeView()
        self.mockUseCase = MockHogeUseCase()
        // モッククラスに置き換え
        self.presenter.view = self.mockView
        self.presenter.useCaseMock = self.mockUseCase
    }
}

スタブの生成

次に、各テストケースの中でスタブを生成します。スタブによってメソッドの振る舞いを都合の良いものに置き換えます。
スタブはメソッド×引数ごとに作ります。指定したもの以外の引数でメソッドが呼ばれた場合エラー(テスト失敗)になります。

スタブの生成
extension HogePresenterTests {
    
    func test_requestData() {
        
        // テストデータを作っておく
        let data = HogeData()
        // mockUseCaseのexecute()が呼ばれたら上のテストデータを返す
        stub(self.mockUseCase) { stub in
            when(stub.execute()).thenReturn(data)
        }
        // mockViewのshowData()が呼ばれたら何もしない
        // 引数は上で作っているテストデータ
        stub(self.mockView) { stub in
            when(stub.showData(data)).thenDoNothing()
        }
        
        ...
    }
}

テストメソッドの実行

テストしたいメソッドを叩いてください。

テストメソッドの実行
extension HogePresenterTests {
    
    func test_requestData() {
        ...
        self.presenter.requestData()
        ...
    }
}

結果の検証

検証用にverify()というメソッドが用意されており、メソッドが呼ばれた回数と渡された渡された引数の確認ができます。回数が1回であることを確認したい場合、回数の指定は省略できます。

結果の検証
extension HogePresenterTests {
    
    func test_requestData() {
        ...
        // mockViewのshowData()がdataという引数で1回呼ばれたことの確認
        verify(self.mockView, times(1)).showData(data)
    }
}

テストデータをMatchableに適合

verify()を使おうとしたらエラーが出る場合があります。verify()はMatchableというプロトコルに適合したデータしか比較することができません。
BoolやString、Intといった基本的なクラスはフレームワーク側でMatchableに適合させてくれていますが、カスタムクラスは自分で適合させなければいけません。
カスタムクラスのExtensionを作り、Matchableに適合することを宣言し、matcherというプロパティを作ります。Equatableに適合させる必要もあります。
GitHubのREADMEにはequalというメソッドを用意すれば良いと書いてあるのですが、こちらのやり方はよくわからずうまく行きませんでした。。。
https://github.com/Brightify/Cuckoo#b-custom-matchers

テストデータをMatchableに適合
// クラスの場合
extension HogeData: Matchable {
    public var matcher: ParameterMatcher<HogeData> {
        return equal(to: self)
    }
}

// 配列の場合
extension Array: Matchable {
    public var matcher: ParameterMatcher<Array<Element>> {
        if let casted = self as? [HogeData] {
            return equal(to: casted) as! ParameterMatcher<Array<Element>>
        }
        else {
            return ParameterMatcher<Array<Element>>()
        }
    }
}

テストケース全体

テストケース全体
extension HogePresenterTests {
    
    func test_requestData() {
        
        let data = HogeData()
        stub(self.mockUseCase) { stub in
            when(stub.execute()).thenReturn(data)
        }
        stub(self.mockView) { stub in
            when(stub.showData(data)).thenDoNothing()
        }
        
        self.presenter.requestData()
        
        verify(self.mockView, times(1)).showData(data)
    }
}

使っていて疑問に思うこと

ファイル生成するたびに日付が入るのでgitで差分ができる

これ相当うざいなーと思ってRun Scriptでその行を削除するようにしてるのですが、皆さんどうしているんでしょうか?

削除はこんな感じ
sed -i -e '/MARK:/d' "${OUTPUT_FILE}"

モックファイルがすごい大きくなる

今はモックファイルを1つだけ生成するようにしているのですが、行数が素敵なことになっています。
分割しようと思っても、全画面で使うような共通プロトコルがある場合、うまく分けることができません。
編集するわけでも無いので別にファイルサイズが大きくなってもいいっちゃいいんですが、皆さんどうしているんでしょうか?

まとめ

CuckooはiOS版Mockito。Run Scriptを使ってモックファイルを生成してくれる。
テストはメソッドをスタブによって振る舞いを変え、verify()を使って結果を検証する。
テストデータはMatchableに適合させる必要がある。

終わりに

今までモックを自分で書いていたので、それに比べるとすごい楽になったし、テスト自体に集中できるので、非常に良いフレームワークだと思います:clap:
ググった感じ日本語の資料はかなり少ないので、参考になれば幸いです:muscle:

参考

SwiftのモックライブラリCuckoo
Staying Sane with Cuckoo and Code Generation

48
30
1

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
48
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?