はじめに
UIのユニットテストを書く時の阻害要因になるのがSVProgressHUDの使用箇所(使っている場合)。こいつをなんとかモック化してみました。
問題
OCMockを使ってSVProgressHUDをモック化する場合、普通、以下の感じになります。
@implementation MockSVProgressHUD
+ (void)startMocking {
mock = OCMClassMock([SVProgressHUD class]);
[[[mock stub] andCall:@selector(showSuccessWithStatus:) onObject:self] showSuccessWithStatus:[OCMArg any]];
[[[mock stub] andCall:@selector(showErrorWithStatus:) onObject:self] showErrorWithStatus:[OCMArg any]];
[[[mock stub] andCall:@selector(dismiss) onObject:self] dismiss];
}
+ (void)showSuccessWithStatus:(nullable NSString*)status {
}
+ (void)showErrorWithStatus:(nullable NSString*)status {
}
+ (void)dismiss {
}
ところが、dismissメソッドはクラスメソッドとインスタンスメソッドの両方があり、その場合、SVProgressHUDのdismissメソッドが呼ばれます。さてさて
解決策
というわけで、SVProgressHUDのソースを読むと以下のようにシングルトンになっています。
+ (SVProgressHUD*)sharedView {
static dispatch_once_t once;
static SVProgressHUD *sharedView;
#if !defined(SV_APP_EXTENSIONS)
dispatch_once(&once, ^{ sharedView = [[self alloc] initWithFrame:[[[UIApplication sharedApplication] delegate] window].bounds]; });
#else
dispatch_once(&once, ^{ sharedView = [[self alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; });
#endif
return sharedView;
}
以下のようにクラスメソッドからはそのシングルトンを直接、あるいは間接的に呼び出しています。
+ (void)setViewForExtension:(UIView*)view {
[self sharedView].viewForExtension = view;
}
ということはそのsharedViewメソッドのreturn値をnilにすれば、とりあえずモックとして機能します。実装例を示します。
// プライベートメソッドを強引に開示する。
@interface SVProgressHUD(Testing)
+ (SVProgressHUD *)sharedView;
@end
@implementation MockSVProgressHUD
+ (void)startMocking {
mock = OCMClassMock([SVProgressHUD class]);
[[[mock stub] andCall:@selector(sharedView) onObject:self] sharedView];
}
...
// nilを返す事によってMock化する。
+ (SVProgressHUD *)sharedView {
return nil;
}
無事、モック化できました。もっと挙動を制御したい時、テストスパイに仕立てたい時は、nilを返すのではなく、偽装するオブジェクトを返して各種インスタンスメソッドを実装する必要があるでしょう。
課題
- OCMockを勉強すればもっとうまい方法があるかも。
- SVProgressHUDの内部実装に依存する方法なので、将来バージョンアップして内部実装が変わった時にこの策は破綻するかも?。