スタブメソッドの戻り値を動的に変更する

こんにちは。マサモトです。

-- 宣伝ここから --

只今、ミュート・下書き・ユーザー名補完など盛りだくさんの新機能を搭載した feather for Twitter v2.1.0 を申請中です! 公開までもうしばらくお待ちください! ([2014-06-25 追記] 公開されました!)

-- 宣伝ここまで --

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];
}