こんにちは。マサモトです。
OCMock は -andReturn: を使ってスタブメソッドの戻り値を指定できます。
こんな感じ。
SampleProtocol.h
@protocol SampleProtocol <NSObject>
- (NSString *)getString;
@end
StubReturnTest.m
id mock = [OCMockObject mockForProtocol:@protocol(SampleProtocol)];
[[[mock stub] andReturn:@"aaa"] getString];
XCTAssertEqualObjects([mock getString], @"aaa");
戻り値が固定でいい場合はこれで問題ありませんが、delegate のモックを作る際などに動的に戻り値を変えたい場面があったりします。
-andDo: を使って戻り値を動的に変更する
スタブメソッドの呼び出しをフックできる -andDo: を使い、フック時に渡される NSInvocation を直接操作することで戻り値を動的に変更できます。
DynamicReturnTest.m
id mock = [OCMockObject mockForProtocol:@protocol(SampleProtocol)];
// この値が -getString の戻り値になる
__block NSString *retString;
// -getString が呼び出された時に戻り値を設定
[[[mock stub] andDo:^(NSInvocation *i) {
[i setReturnValue:&retString];
}] getString];
retString = @"aaa";
XCTAssertEqualObjects([mock getString], @"aaa");
retString = @"bbb";
XCTAssertEqualObjects([mock getString], @"bbb", @"変わってる!");
こんな感じです。 戻り値がプリミティブ型の場合でも同じように対応できます。
NSInvocation からスタブメソッドに渡された引数を取得できるので、引数によって戻り値を変えたりすることもできます。 (あまり複雑になるようだと普通にモック用のクラスを作成した方がよさそうですが)
-andDo: を使う場合の注意点 (循環参照)
TestCase クラスのメンバ変数としてモックオブジェクトを保持し、そのモックの -andDo: から TestCase (またはそのメンバ変数) を参照している場合、循環参照となってメモリリークしてしまいます。
メモリリークするコード
@implementation MockLeakTests {
id _mock;
NSString *_retString;
}
- (void)setUp {
[super setUp];
_mock = [OCMockObject mockForProtocol:@protocol(SampleProtocol)];
// self と _mock が循環参照してる!
[[[_mock stub] andDo:^(NSInvocation *i) {
[i setReturnValue:&_retString];
}] getString];
}
@end
対策としては、TestCase の -tearDown でモックを解放するか、
- (void)tearDown {
// 循環参照を断ち切る
_mock = nil;
[super tearDown];
}
または -andDo: からは self を weak で参照するようにします。
- (void)setUp {
[super setUp];
_mock = [OCMockObject mockForProtocol:@protocol(SampleProtocol)];
// self を weak で参照
__weak typeof(self) wself = self;
[[[_mock stub] andDo:^(NSInvocation *i) {
__strong typeof(wself) self = wself;
if (self) {
[i setReturnValue:&self->_retString];
}
}] getString];
}