[iOS] OCMock でスタブメソッドの引数のブロック構文を実行する

2015.06.24

ユニットテストでブロック構文を実行するには?

今回は OCMock を使った場合のちょっとした Tips のお話をします。

例えば、以下のようなメソッドがあるとします。非同期処理を実行するときなどでよく見かけるメソッドですね。ブロック構文を引数で渡しておき、メソッド内の非同期処理実行後にそのブロック構文を実行することで非同期処理の待ち合わせを実現します。

@interface Sample : NSObject

- (void)saveInBackgroundWithBlock:(void (^)(BOOL succeed))block;

@end

@implementation Sample

- (void)saveInBackgroundWithBlock:(void (^)(BOOL succeed))block {
// 何らかの非同期処理…
dispatch_async(dispatch_get_main_queue(), ^{
// ブロック構文を呼び出し
block(YES);
});
}

@end

さて、このメソッドをユニットテストでスタブしたい場合どうすれば良いでしょうか? saveInBackgroundWithBlock:を呼び出す場合、引数で渡したブロック構文内で後続の処理を実装していることが多いと思います。しかし、このメソッドをスタブしただけでは引数に渡されたブロック構文は実行されないため、後続の処理は実行できません。

NSInvocation でブロック構文を実行する

そこで活躍するのが NSInvocation です。NSInvocation は Objective-C でオブジェクト同士のメッセージングを静的に取り扱うためのクラスです。NSInvocation を OCMock で活用すると、スタブしたメソッドに渡された引数を取り出すことができます。

それでは早速テストケースです。waitUntil は Specta で用意されている非同期テストの待ち合わせのための構文です。

waitUntil(^(DoneCallback done) {
// ① NSInvocation の作成
void (^invocation)(NSInvocation *) = ^(NSInvocation *invocation) {
__unsafe_unretained void(^block)(BOOL);
[invocation getArgument:&block atIndex:2];
block(NO);
};
// ② モックオブジェクトの作成
id mock = OCMClassMock([Sample class]);
// ③ メソッドの差し替え
OCMStub([mock saveInBackgroundWithBlock:OCMOCK_ANY]).andDo(invocation);
// ④ メソッドの実行
[mock saveInBackgroundWithBlock:^(BOOL succeed) {
expect(succeed).to.equal(NO);
done();
}];

});

解説します。まず①で NSInvocation を引数に取るブロック構文を作成しています。OCMock ではモックオブジェクト(②)にこのブロック構文を andDo で渡すことで(③)、スタブ対象のメソッドが呼び出された時にそのブロック構文を実行してくれます。このブロック構文には、呼び出されたメソッドの NSInvocation オブジェクトが渡されます。こうして①のブロック構文で受け取った NSInvocation オブジェクトの getArgument:atIndex: で引数のブロック構文を取り出し、自由に実行することが出来ます。getArgument:atIndex: の引数は、第一引数が2になっているので注意してください(0と1は暗黙的な引数で、 self_cmd が入っています)。

まとめ

ちょっとした Tips でした。いざスタブしようとするとハマるので、覚えておいて損はないと思います。

参考