テストコードを書いているとき、テスト対象のオブジェクトの振る舞いを変えるには色々方法があります。
今回はOCMock + NSInvocationクラスを引数にしたブロックの組み合わせで振る舞いを変える方法を説明します。
基本的なやり方
まずは以下のようなクラスをテストするとして、引数も戻り値もないメソッドの振る舞いを変えてみます。
@interface Schedule
- (void)syncWithServer;
- (NSString*)monthlySummary;
- (void)add:(NSString*)eventName eventDate:(NSDate*)date location:(NSString*)address snooze:(BOOL)snooze;
- (NSString*)parseWithGivenBlocks:(void^(blockAtStart)(NSData*)) completion:(BOOL^(endBlock)(int));
@end
@implementation Schedule
- (void)syncWithServer
{
// サーバーと同期する処理を行う
}
- (NSString*)monthlySummary
{
// 月ごとのサマリを出力する
}
- (void)add:(NSString*)eventName eventDate:(NSDate*)date location:(NSString*)address snooze:(BOOL)snooze
{
// eventNameのスケジュールを追加する処理を行う
}
- (NSString*)parseWithGivenBlocks:(void^(blockAtStart)(NSData*)) completion:(BOOL^(endBlock)(int))
{
// 渡されたブロックを使って何かしらパースする処理が実行される
}
@end
例として、syncWithServerというメソッドはサーバーと通信して何かしらの処理を実行しそうなので、
ユニットテストではそのような処理を実行しないようスタブしてみます。
void (^syncWithServerBlock)(NSInvocation*) = (NSInvocation* invocation) {
// 何かしら振る舞いを変えたい処理
}
id scheduleMock = [OCMockObject partialMockForObject:[Schedule alloc]];
[[[scheduleMock stub] andDo:syncWithServerBlock] syncWithServer];
上記のようにモックしておくと、syncWithServerが呼び出されたときにsyncWithServerBlockの処理が実行されます。
戻り値を設定する
次に、ブロックで実施した処理結果を戻り値として使いたい場合、ブロック中でNSInvocationのsetReturnValue:を使うと設定できます。
void (^monthlySummaryBlock)(NSInvocation*) = (NSInvocation* invocation) {
// 何かしら処理を実行して、NSStringのsummaryに戻り値がセットされる
[invocation setReturnValue:(void*)&summary];
}
id scheduleMock = [OCMockObject partialMockForObject:[Schedule alloc]];
[[[scheduleMock stub] andDo:monthlySummaryBlock] monthlySummary];
上記のようにモックしておくと、monthlySummaryが呼び出されたときにmonthlySummaryBlockの処理が実行され、summaryに設定された文字列を戻り値として取得できます。
引数を使ってみる
NSInvocationを使うと、メソッド呼び出し時の引数も取得することができます。
スタブするブロックの中で、add: の1番目と2番目の引数だけ使いたいという場合は以下のように書くことができます。
void (^addBlock)(NSInvocation*) = (NSInvocation* invocation) {
NSString* name;
NSDate* date;
[invocation getArgument:&name atIndex:2];
[invocation getArgument:&date atIndex:3];
}
id scheduleMock = [OCMockObject partialMockForObject:[Schedule alloc]];
[[[[scheduleMock stub] ignoringNonObjectArgs] andDo:addBlock] add:OCMOCK_ANY date:OCMOCK_ANY location:OCMOCK_ANY snooze:YES];
getArgument: ですが、0・1のインデックスにはあらかじめself、_cmdが設定されているので、
2以降のインデックスを使うと引数を取得できます。
引数として渡されたブロックの処理を実行してみる
引数がブロックの場合でも、同じように取得することができます。
下の例では、parseWithGivenBlocks:の2番目のブロックだけ実行したい、という場合を示します。
void (^parseBlock)(NSInvocation*) = (NSInvocation* invocation) {
__unsafe_unretained BOOL^(endBlock)(int);
[invocation getArgument:&endBlock atIndex:3];
BOOL returnValue = endBlock(0);
[invocation setReturnValue:(void*)&returnValue];
}
id scheduleMock = [OCMockObject partialMockForObject:[Schedule alloc]];
[[[[scheduleMock stub] ignoringNonObjectArgs] andDo:parseBlock] parseWithGivenBlocks];
parseBlockの中では、引数として渡されたブロックを__unsafe_unretained修飾子をつけた変数に代入しています。
OCMockとNSInvocationを組み合わせて使う場合、モックしたオブジェクトを破棄するときにendBlockを破棄できないことがあるので、いったんARCの管理外にしてしまいます。
本来なら、endBlockはメモリリークの懸念等があるのでARCの管理下に置くべきです。
ただ今回の例では、
- テスト用のモックオブジェクトなので、仕方なく許容する
- このブロックの処理をクラスメソッドには使わない
- 一部のテストケースでだけしか使わない
などを判断した結果、上記の方法でテストコードを実装したということにします。
(状況に応じて何を優先するか変わってくるので、上の条件は鵜呑みにせず自分で考えてください)
別な方法で同じようなことを実現したい
こちらの記事にもある通り、メモリ管理等の面からNSInvocationはあまり使わない方が良いと勧められています。
同じような方法を実現する手段として、NSObjectの -methodForSelector が勧められています。
この方法を使わない、という場合にはmethodForSelectorを使うことも検討してみてください。
参考資料
NSInvocation Class Reference
StackOverFlow: @selector - With Multiple Arguments?