Swiftのモック生成ライブラリ「Mockolo」のセットアップ&操作方法

## はじめに

Xcodeにはモック生成機能が搭載されておらず、手動で実装するのが大変だと感じてきたため、導入することにしました。

## 「Mockolo」とは？

Swift用のモック生成ライブラリです。

~~現在はプロトコルのモック生成のみ対応しており、クラスのモック生成は追加予定とのことです。~~
1.1.3でクラスのモック生成もサポートされました。
https://github.com/uber/mockolo/releases/tag/1.1.3

## 環境

- OS：macOS Catalina 10.15.2
- Swift：5.1.3
- Xcode：11.3 (11C29)
- Mockolo：1.1.1

## セットアップ

### Mockoloのインストール

[Mint](https://github.com/yonaskolb/Mint)からインストールします。

```diff:Mintfile
+ uber/mockolo@1.1.1
```

```shell-session
$ mint bootstrap
```

手動でインストールするには、公式ドキュメントをご参照ください。
https://github.com/uber/mockolo#build--install

### ビルド時にモックを生成するようにする

Mockoloはモックの生成時間が速いため、ビルドするたびにモックを生成するようにします。
ビルド時間が気になる場合、手動でコマンドを実行してモックを生成してください。

__Xcodeでプロジェクトを開く
TARGETSで製品ターゲットを選択 > Build Phases > +をクリック > New Run Script Phase >
ドラッグ&ドロップで「Compile Sources」の直前に移動__
<img width="1011" alt="スクリーンショット_2020-01-03_16_43_59.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/c55270d8-2a2f-547e-42b4-6a7cd6296123.jpeg">

スクリプトは「Generate Mocks with Mockolo」のようにわかりやすい名前を付けるといいです。

展開して以下のスクリプトを記述します。

```shell-session
if which mint >/dev/null; then
  rm -f $SRCROOT/MockResults.swift
  mint run mockolo mockolo --sourcedirs $SRCROOT/{製品ターゲット名} --destination $SRCROOT/MockResults.swift
else
  echo "warning: Mint not installed, download from https://github.com/yonaskolb/Mint"
fi
```

すでにモックがあるとビルドエラーになることがあるため、生成前に `rm` で削除しています。

__Output Files > +をクリック__
`--destination` で指定しているファイルパスを記述します。

```
$SRCROOT/MockResults.swift
```

生成されるファイルを記述しないと、CI/CD時に以下のエラーが発生します。

```
error: Build input file cannot be found:
```

以下の記事を参考にさせていただきました。
https://qiita.com/lovee/items/fa3ef5e60cfbf31996c0

<img width="849" alt="スクリーンショット_2020-01-04_12_16_45.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/2ee57566-4125-52e2-6cbf-f941e56d7fd5.jpeg">

Mintを使っていない場合、 `mint run mockolo` を外し、if文の条件を変更してください。

使っているオプションを説明します。
以下の2つは必須であり、必要に応じて値を変更してください。

|オプション|説明|
|:--|:--|
|--sourcedirs|生成対象のフォルダパス<br />製品ターゲット名のフォルダを指定すれば、通常は全ファイルを対象にできる|
|--destination|モックの生成パス|

`--mock-final` は任意ですが、私は付けるのが好みです。

|オプション|説明|
|:--|:--|
|--mock-final|モックに `final` を付ける<br />1.2.8で追加された|

その他のオプションは[公式ページ](https://github.com/uber/mockolo#run)または `mockolo --help` をご参照ください。

プロジェクトをビルドし、「$SRCROOT(通常はプロジェクトのルートフォルダ)」に「MockResults.swift」が生成されたら、プロジェクトにドラッグ&ドロップします。
<img width="275" alt="スクリーンショット_2020-01-03_16_53_20.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/21e9e6a1-a188-a7a8-5a6e-951cf3881045.jpeg">

[Copy items if needed]チェックをOFFにし、[Finish]をクリックします。
<img width="737" alt="スクリーンショット_2020-01-03_16_51_53.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/6d65a4dc-6a47-7a91-3604-94fb973afbc9.jpeg">

### バージョン管理から無視する

不要な競合を防ぐため、生成された「MockResults.swift」をバージョン管理の対象外にします。

Gitを使っている場合、以下を「.gitignore」に追加するのみでOKです。

```diff:.gitignore
+ MockResults.swift
```

## 操作方法

モックを生成したいプロトコルに `@mockable` のドキュメンテーションコメントを付けます。
タイプエイリアスがある場合、カッコ内に書きます。

```swift:Foo.swift
/// @mockable(typealias: T = AnyObject; U = StringProtocol)
public protocol Foo {
    associatedtype T
    associatedtype U: Collection where U.Element == T 
    associatedtype W 

    var num: Int { get set }

    func bar(arg: Float) -> String
}
```

ビルドすると、モックが生成されます。

```swift:MockResults.swift
// クラス名は `{プロトコル名}Mock` となる
public class FooMock: Foo {
    typealias T = AnyObject
    typealias U = StringProtocol
    typealias W = Any // 指定しないと `Any` になる

    init() {}
    init(num: Int = 0) {
        self.num = num
    }

    var numSetCallCount = 0
    var underlyingNum: Int = 0
    var num: Int {
        get {
            return underlyingNum
        }
        set {
            underlyingNum = newValue
            numSetCallCount += 1
        }
    }

    var barCallCount = 0
    var barHandler: ((Float) -> (String))?
    func bar(arg: Float) -> String {
        barCallCount += 1
        if let barHandler = barHandler {
            return barHandler(arg)
        }
        return ""
    }
}
```

自動生成されたコードのうち、テストで使うプロパティのみ説明します。

|プロパティ|説明|
|:--|:--|
|{プロパティ名}SetCallCount|セッターの呼び出し回数|
|{メソッド名}CallCount|メソッドの呼び出し回数|
|{メソッド名}Handler|メソッドの呼び出し時に実行されるクロージャ|

テスト時は以下のように使います。

```swift:FooTests.swift
func testMock() {
    // モックを生成する
    let mock = FooMock(num: 5)

    // 対象プロパティのセット回数を確認する
    XCTAssertEqual(mock.numSetCallCount, 1)

    // ハンドラは対象メソッドの呼び出し前に自分で代入する
    mock.barHandler = { arg in
        return String(arg)
    }

    // 対象メソッドの呼び出し回数を確認する
    XCTAssertEqual(mock.barCallCount, 0)
}
```

## おわりに

とても簡単にモックを生成できました！
これでテスト時にVIPERのモックを手動で実装する手間が省けるぞ😊

もっと早く導入すればよかったと思いました笑

## 参考リンク

- [uber/mockolo: Efficient Mock Generator for Swift](https://github.com/uber/mockolo)
- [R.swiftのセットアップ方法(Swift5) - Qiita](https://qiita.com/uhooi/items/82fbdd94bdc467a22422)