#はじめに
最近になってOCMockを使ってみたのでメモ。
思ったよりも日本語でまとまった情報がなくて、最初に学習コストがかかったので自分なりにまとめてみる。
詳しい情報については公式ページのリファレンスに記載されているのでそちらを参照のこと。
http://ocmock.org/reference/
(少しずつ書き足していく・・・かも)
#概要
Objective-C向けのMockライブラリ。
http://ocmock.org/
XCTestなどでユニットテストを書く際に、依存したオブジェクトをMock化して依存関係を断ち切ってテストできる。(ユニットテストやMockについては本題とずれるので割愛)
##制限事項
Swiftで書かれたコードに対しても利用することはできるが、テストコード自体はObjective-C(XCTestなど)で書く必要がある。
Tests have to be written in Objective-C
他にも細かい制限事項はあったりするが、個人的にこれが一番大きな制限事項。
##導入
CocoaPods経由で導入できる。
#基本
##OCMockで出来ること
あるオブジェクト(クラス)をMock化することで
- 実際のメソッドの代わりに 別の処理を行う(別の値をReturnする)
- あるメソッドが 呼び出されたか検証する
といったことが出来る。
##Mockオブジェクトの活用
A --> B --> C といったクラスの依存関係があり、クラスAをテストしたい場合のケースを考える。
そうした場合、クラスBをMock化することで、クラスA単体でテスト出来るようになり、クラスCへの依存関係を断ち切ってテストできるようになる。
A --> B (mock)
具体的には、
- クラスBのあるメソッドが呼び出された時に、 任意の値が返されるようにする
- クラスAのあるメソッドを呼び出した時に、 クラスBのあるメソッドが呼び出されたかを検証する
といったことが可能となる。
##2種類の書き方
以下の2種類の書き方がある。
- Objective-C標準のメッセージ構文を利用した書き方
- マクロを利用したモダンなシンタックスを利用した書き方(OCMock3以降)
id mock = [OCMockObject partialMockForObject:foo];
[[[mock stub] andReturn:@"foo!"] bar];
id mock = OCMPartialMock(foo);
OCMStub([mock bar]).andReturn(@"foo!");
公式では以下の3点の理由からマクロ版(モダンシンタックス)を推奨している。
http://ocmock.org/ocmock3/
- より正確なエラー報告(Xcode/AppCode)
- 他のテストフレームワークと相性が良い
- Mockとメッセージ呼び出しが見た目的に分離できる(=可読性が高い)
マクロ版はXcode上でのコード補完がやや弱いように感じたが、上記のようなメリット(特に可読性)が得られるので、マクロ版で統一するほうが良いと感じた。(少なくともプロジェクト内ではどちらの書き方に統一したほうが良いだろう)
以降、サンプルコードは全てマクロ版(モダンシンタックス)で記す。
#使い方
##Mockの作成
クラスから作成する方法と、生成済みのオブジェクトから作成する方法がある。
id mock = OCMClassMock([NSString class]);
Foo *foo = [[Foo alloc] init];
id mock = OCMPartialMock(foo);
なお、どちらで生成しても後述の「スタブ」と「メソッド呼び出し検証」は同じように使える。
##スタブ
Mockに対してメソッドが呼び出された時に、 実際の処理の代わりに別の処理をさせることが出来る。あるメソッドが呼び出された時に、任意の値を返すようにするには以下のようにする。
// NSStringクラスからMockを作成し
id mock = OCMClassMock([NSString class]);
// stringByAppendingStringが呼び出された場合に、"foo"という文字列を返す
OCMStub([mock stringByAppendingString:OCMOCK_ANY]).andReturn(@"foo");
###構文
OCMStub()
の中でメッセージ呼び出し
を記述して、それに対するアクションandReturn
を.
で区切って指定している。
つまりスタブは以下の構文になっている。
OCMStub(メッセージ呼び出し).アクション
###メッセージ呼び出しのマッチ
メッセージ呼び出しに引数が含まれていた場合、 引数も含めて完全に一致した場合にマッチしたと認識される。(結構ハマりやすいポイント)
例えば、以下のコードでは引数に@"bar"
が指定された場合にだけ、本来のメソッドではなく.andReturn
で指定された値@"foo"
が返る。
OCMStub([mock stringByAppendingString:@"bar"]).andReturn(@"foo");
###OCMOCK_ANY
しかし、実際にモックを利用する際はメソッドの引数は何でも良いことが多い。そうした場合には、スタブのサンプルコードで示したようなOCMOCK_ANY
を指定する。
「nilの場合」と「特定のクラス型の場合」とか細かく指定することも出来る。そのあたりの詳細は公式リファレンスの4.3 Matching arguments
を参照。
####Note
プリミティブ型の引数について、どんな値が入ってきてもマッチさせる方法はあるか?
##スタブの種類(代表的なもの)
###戻り値の変更
OCMStub([mock someMethod]).andReturn(anObject);
###他メソッドの呼び出し
OCMStub([mock someMethod]).andCall(anotherObject, @selector(method));
###Blocksの呼び出し
OCMStub([mock someMethod]).andDo(^(NSInvocation *invocation) {
// do something
});
###Notificationを送信
OCMStub([mock someMethod]).andPost(notification);
###例外をスロー
OCMStub([mock someMethod]).andThrow(exception);
##メソッド呼び出しの検証
メソッドが呼び出されたかどうか検証することが出来る。
TODO: そのうち書く