はじめに
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からインストールします。
+ uber/mockolo@1.1.1
$ mint bootstrap
手動でインストールするには、公式ドキュメントをご参照ください。
https://github.com/uber/mockolo#build--install
ビルド時にモックを生成するようにする
Mockoloはモックの生成時間が速いため、ビルドするたびにモックを生成するようにします。
ビルド時間が気になる場合、手動でコマンドを実行してモックを生成してください。
Xcodeでプロジェクトを開く
TARGETSで製品ターゲットを選択 > Build Phases > +をクリック > New Run Script Phase >
ドラッグ&ドロップで「Compile Sources」の直前に移動
スクリプトは「Generate Mocks with Mockolo」のようにわかりやすい名前を付けるといいです。
展開して以下のスクリプトを記述します。
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
Mintを使っていない場合、 mint run mockolo
を外し、if文の条件を変更してください。
使っているオプションを説明します。
以下の2つは必須であり、必要に応じて値を変更してください。
オプション | 説明 |
---|---|
--sourcedirs | 生成対象のフォルダパス 製品ターゲット名のフォルダを指定すれば、通常は全ファイルを対象にできる |
--destination | モックの生成パス |
--mock-final
は任意ですが、私は付けるのが好みです。
オプション | 説明 |
---|---|
--mock-final | モックに final を付ける1.2.8で追加された |
その他のオプションは公式ページまたは mockolo --help
をご参照ください。
プロジェクトをビルドし、「$SRCROOT(通常はプロジェクトのルートフォルダ)」に「MockResults.swift」が生成されたら、プロジェクトにドラッグ&ドロップします。
[Copy items if needed]チェックをOFFにし、[Finish]をクリックします。
バージョン管理から無視する
不要な競合を防ぐため、生成された「MockResults.swift」をバージョン管理の対象外にします。
Gitを使っている場合、以下を「.gitignore」に追加するのみでOKです。
+ MockResults.swift
操作方法
モックを生成したいプロトコルに @mockable
のドキュメンテーションコメントを付けます。
タイプエイリアスがある場合、カッコ内に書きます。
/// @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
}
ビルドすると、モックが生成されます。
// クラス名は `{プロトコル名}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 | メソッドの呼び出し時に実行されるクロージャ |
テスト時は以下のように使います。
func testMock() {
// モックを生成する
let mock = FooMock(num: 5)
// 対象プロパティのセット回数を確認する
XCTAssertEqual(mock.numSetCallCount, 1)
// ハンドラは対象メソッドの呼び出し前に自分で代入する
mock.barHandler = { arg in
return String(arg)
}
// 対象メソッドの呼び出し回数を確認する
XCTAssertEqual(mock.barCallCount, 0)
}
おわりに
とても簡単にモックを生成できました!
これでテスト時にVIPERのモックを手動で実装する手間が省けるぞ😊
もっと早く導入すればよかったと思いました笑