はじめに
こんにちは、みなさん、単体テスト書くのは大好きですよね?
swift歴3ヶ月目にしてSwiftのテストタブルなコードを、自前で書くことに面倒臭さを感じていたので、何か良いライブラリはないかな〜。と探していたところ、Cuckooという、MockGenerateライブラリがある事をrealmのtry! Swift翻訳ページで知りました。(2017年3月のTry! SwiftTokyoなのでもともとswiftをやっている方はみんな知っている?)
早速CuckooのREADME.mdを見に行ったところ、CuckooはJavaの単体テストフレームワークである、Mockito ライクなテストコードが書ける事が判明しました。
もともとJavaエンジニアな私にとって使い慣れているMockitoとほぼ同じ書き方ができるのは、非常に嬉しく早速試してみましたので、簡単な導入方法と、どんな事が出来るのか?をまとめたいと思います。
Cuckooって?
オープンソースのモック自動生成フレームワークです。
利用することにより、Mockの自動生成、Stubの用意が楽。テストコードが書きやすくなる。などの機能があります。
導入方法
早速簡単にCarthageの導入方法をまとめます。
前提として、一度はCarthageによるフレームワークの導入を行ったことのある方。 RunScriptを導入した事のある方向けにざっくりと記載しています。
環境情報
Xcode 9.4.1
SwiftKit/Cuckoo 0.11.3
導入
Cartfileに定義
通常のフレームワーク同様、 Cartfile
に以下を記載します
github "SwiftKit/Cuckoo"
Mock化したいクラスがあるProjectのRunscriptにMockGenerateするためのスクリプトを定義する
OUTPUT_FILE="$PROJECT_DIR/${PROJECT_NAME}Tests/GeneratedMocks.swift"
INPUT_DIR="${PROJECT_DIR}/${PROJECT_NAME}"
"Carthage/Checkouts/Cuckoo/run" generate --testable "$PROJECT_NAME" \
--output "${OUTPUT_FILE}" \
"$INPUT_DIR/HogeHuga.swift" # ここにMock化したい自身のプロジェクトのプロジェクトファイルを定義する
echo "$SRCROOT"
- Mock化対象プロジェクトのRunscriptに上記を記載します
- これは、先述したように、Cuckooのアプローチは、MockをGenerateし、GenerateされたMockによりStubbingをするというライブラリなため、必要な作業となります。
- Generateされるファイルは、1行目の
OUTPUT_FILE
の先に吐かれるようになるため、Generateされたファイルを1度Add file
してあげる必要があります。
テストプロジェクトのLink Binary With LibrariesにCuckooを追加
- 上記のように、Carthageで落として来た
Cuckoo.framewor
を指定し、追加してください。
準備完了
ここまで出来たら、1度ビルドします。
すると、先ほどのRun Scriptに記載した OUTPUT_FILE
に指定した出力先をFinderやコマンドウィンドウなどで確認すれば、 GeneratedMocks.swift
というファイルが出来上がっているかと思いますので、テストプロジェクトに対して Add File to projectname...
します。
テストにおける使い方
ここからは、実際に生成されたMockを利用してStubbingのやり方を記載していきます。
検証用のクラスたち
検証用の適当に作った CuckooGenerator.swift
をMockGenerateします。
依存関係は、 ViewModel
から、 CuckooGenerator
を呼び出し、何かの処理をDelegateする. というようにしました。
意味のわからない処理を書いていますが、検証用という事で見逃してください...泣
/// Mock化対象のクラス
import Foundation
internal enum CuckooType {
case joy
case angry
case piyo
case crow
case normal
}
internal class CuckooGenerator {
func generate(_ source: String, type: CuckooType) -> String {
switch type {
case .angry:
return source + "!!!!!!!!!!!"
case .joy:
return source + "^_^"
case .normal:
return source
default:
return "ぴよぴよカアカア..."
}
}
}
/// Mock化対象クラスを利用する呼び出し側のクラス
import Foundation
internal protocol ViewModelDelegate: class {
func viewModel(_ vm: ViewModel, shoudShowCuckooLabel cuckoo: String)
func viewModel(_ vm: ViewModel, shoudShowAngryLabel cuckoo: String)
func viewModel(_ vm: ViewModel, shoudShowJoyLabel cuckoo: String)
}
internal class ViewModel {
weak var delegate: ViewModelDelegate?
let cuckooGenerator: CuckooGenerator
init(cuckooGenerator: CuckooGenerator = CuckooGenerator()) {
self.cuckooGenerator = cuckooGenerator
}
func tapCuckooButton(_ cuckoo: String) {
delegate?.viewModel(self, shoudShowCuckooLabel: cuckooGenerator.generate(cuckoo, type: .normal))
}
func tapAngryButton(_ cuckoo: String) {
delegate?.viewModel(self, shoudShowAngryLabel: cuckooGenerator.generate(cuckoo, type: .angry))
}
func tapJoyButton(_ cuckoo: String) {
delegate?.viewModel(self, shoudShowJoyLabel: cuckooGenerator.generate(cuckoo, type: .joy))
}
}
テストクラス
以下がCuckooによりGenerateしたMockを利用したテストクラスです。
import XCTest
import Cuckoo // テストクラス側ではCuckooをインポートする必要がある.
@testable import cuckooSample
class CuckooSampleTests: XCTestCase {
// MARK: - Tests
private var angryExpectation: XCTestExpectation!
// MockCuckooGeneratorが、CuckooによりGenerateされた CuckooGeneratorのMockクラス
private let mock = MockCuckooGenerator()
lazy var viewModel = {
// ViewModelの生成引数にCuckooによりGenerateされたMockクラスを注入することにより、Stubbingが可能となる
ViewModel(cuckooGenerator: self.mock)
}()
override func setUp() {
super.setUp()
angryExpectation = self.expectation(description: "angryExpectation")
}
override func tearDown() {
super.tearDown()
}
/// cuckooがGenerateしたstubで設定したthenReturnの文字列がdelegateにそのまま引き継がれる事を確認する
func testNormalStubbing() {
// mockの振る舞いを設定する(stubbing)
stub(mock) { mock in
// 引数が何であっても "buhyyyyy" を返却する
when(mock.generate(any() , type: any())).thenReturn("buhyyyyy")
}
viewModel.delegate = self
// viewModelのtapAngryButtonを呼ぶ. CuckooGeneratorがMock化されているため、先ほど設定したwhen()内の振る舞いが適用される
viewModel.tapAngryButton("")
wait(for: [angryExpectation], timeout: 1)
}
}
extension CuckooSampleTests: ViewModelDelegate {
// MARK: Delegate
func viewModel(_ vm: ViewModel, shoudShowCuckooLabel cuckoo: String) {
XCTAssertEqual(cuckoo, "buhyyyyy")
}
func viewModel(_ vm: ViewModel, shoudShowAngryLabel cuckoo: String) {
// when内で設定した振る舞いにより, cuckooはbuhyyyyyが返却されるため、このテストケースは通る
XCTAssertEqual(cuckoo, "buhyyyyy")
angryExpectation.fulfill()
}
func viewModel(_ vm: ViewModel, shoudShowJoyLabel cuckoo: String) {
XCTAssertEqual(cuckoo, "buhyyyyy")
}
}
コメントに色々記載しましたが、個々に見て行きます。
import
import Cuckoo // テストクラス側ではCuckooをインポートする必要がある.
- MockをGenerateするのは、先述した通りにRunScriptで行うため本流のプロジェクトに依存関係は貼らなくても良いのですが、TestクラスでStubbingするには依存関係を貼る必要があります。
Mockクラスの初期化
// MockCuckooGeneratorが、CuckooによりGenerateされた CuckooGeneratorのMockクラス
private let mock = MockCuckooGenerator()
- RunScriptにより生成された
GenerateMocks.swift
の中には、RunScriptで指定したInputFileのPrefixにMock
が付与されたクラスが生成されます。 - なので、本流プロジェクトでBuildを走らせておく +
GenerateMocks.swift
をプロジェクトにAdd File
しておけば、Mock~
まで打った後にEsc
を押下すれば補完候補としてMockクラスが登場します。
Dependencyの差し替え(Mock対象依存クラスの差し替え)
lazy var viewModel = {
// ViewModelの生成引数にCuckooによりGenerateされたMockクラスを注入することにより、Stubbingが可能となる
ViewModel(cuckooGenerator: self.mock)
}()
- 本流側のプロジェクトのInitでProtocolで宣言していなくても、Initializeで差し替える事が出来ます。
- この例では、
private let
で初期化宣言したMockクラスをViewModelのInitに注入してあげることにより、CuckooGenerator
を差し替えてあげています。 - 上記により、
本来テストがしたいクラスの関心事
にのみ注力したテストが書けるようになります。
Stubbing
// mockの振る舞いを設定する(stubbing)
stub(mock) { mock in
// 引数が何であっても "buhyyyyy" を返却する
when(mock.generate(any() , type: any())).thenReturn("buhyyyyy")
}
- これまで記載してきたことにより、Mockの準備は完了しているので、Mockの振る舞いを決めるための処理を書きます。
-
stub
は、Cuckoo
ライブラリの中のファンクションです。ここでは、Cuckoo
は省略していますが、stub
やwhen
の前にCuckoo
と記述してももちろん動きます。 仮にPromiseKitを使っている場合はフレームワーク名を記載しなければ動かないと思います。 -
stub(mock)
の構文により、どのmockをstubするかを決めた後に、when(mock.(${stubbingしたファンクション名とstubbingが発動する引数の型や条件}).thenReturn(${結果として何を返却するのか})
を記述します。 - 例の場合、
when(mock.generate(any(), type: any())).thenReturn("buhyyyyy") となっていますが、これは
generateファンクションの第一、第二引数が何であった場合でも戻り値としてbuhyyyyy
を返却する。という意味合いになります。 -
any()
の部分は、値でも設定できるし型も設定できるしEqutable
も設定できます。
結果的にどうなるか?
ここまでの設定で、 generate
をどんな条件で叩いても、結果は buhyyyy
を返却するというstubが出来上がっているので、以下のDelegateに記載しているXCTAssertEquals
のテストケースは成功となります。
func viewModel(_ vm: ViewModel, shoudShowAngryLabel cuckoo: String) {
// when内で設定した振る舞いにより, cuckooはbuhyyyyyが返却されるため、このテストケースは通る
XCTAssertEqual(cuckoo, "buhyyyyy")
angryExpectation.fulfill()
}
上記までが、Cuckoo
の導入とMock生成の仕方、Stubbingの仕方の一通りの流れです。
いかがでしたしょうか? 正直、ほとんどREADME.mdに書いてある事を割と素直にそのまま書いた感じになってしまいましたが、Cuckoo
自体の雰囲気とその強力さはわかっていただけたかと思います。
当該の記事に書いた事よりもう少し多くのパターンを試したサンプルコードを cuckoo-sample-projectにあげているので、興味のある方は覗いて見てください。
他に検証した結果、できる事がわかった場合、当該リポジトリにプッシュしていきたいと思います。
終わりに
何が嬉しい?
- mock化/振る舞いをstubしたいクラスのProtocolを自分で書かなくてもよくなる
- stubが柔軟に用意出来るようになり、Cuckoo自体がテストコードの書き方を統一してくれる
- valueとしてこの値の時に... 型がこの型の時にこれを返却するという書き方が容易に書ける
- N回目に呼ばれた時に何を返却するか?のような振る舞いが簡単に書ける
- 処理が呼ばれていないこと/呼ばれたことの確認が簡単に行える。これは個人的にはとても嬉しく思いました。 今までは自作Mock側で呼ばれた回数をカウントして1回目の時の期待値、2回目の時の期待値...というようにif文を書いていましたので
出来なさそうなこと
- spy的な振る舞い
- 外部フレームワークの中のfuncの一部のみをstubbingしてテスト可能にする..ということは出来なさそうです
- やるとしたら、外部フレームワークの処理をラップしたクラスを用意して、CuckooでラップクラスをMock化する。という方法になりそうです(試してないので、試したら加筆しますmm)
- READMEを見たら、swiftのアップデートにより、この振る舞いが難しくなってしまった...というような事を書いてありました(雑な意訳