2013年12月11日 星期三

在Objective-C 裡如何測試 Singleton

製作Singleton

Cocoa Samurai的 Singletons: You’re doing them wrong 這篇文章提到了一種良好的singleton建構方式。

+(MyClass *)sharedInstance {
 static dispatch_once_t pred;
 static MyClass *shared = nil;

 dispatch_once(&pred, ^{
  shared = [[MyClass alloc] init];
 });
 return shared;
}

這個寫法的優點有三
1. 在整個app生命週期裡只會被執行一次
2. thread safe
3. 比用@synchronize()快

測試Singleton

現在想像有一個class用到了這個singleton,而當我們要對這個class做unit test,測試是否如我們預期般運作的時候問題就來了。在做unit test的時候,受測物件必須隔離與其他物件的相依性,否則就變成integration test了。我們通常會用mock達成這個目的。

例如我們有下面這段code

- (void)doSomething
{
    [[ArticleManager sharedInstance] doSomethingElse];
    ...
}

顯然的,它只會取得唯一存在的instance,沒有讓你mock的機會。這時可以採用 Objective-C Singleton Pattern Updated For Testability 這篇文章提到的方法,把singleton做一點變形。

@implementation ArticleManager

static ArticleManager *_sharedInstance = nil;
static dispatch_once_t once_token = 0;

+(instancetype)sharedInstance {
    dispatch_once(&once_token, ^{
        if (_sharedInstance == nil) {
            _sharedInstance = [[ArticleManager alloc] init];
        }
    });
    return _sharedInstance;
}

+(void)setSharedInstance:(ArticleManager *)instance {
    once_token = 0; // resets the once_token so dispatch_once will run again
    _sharedInstance = instance;
}

@end

在做測試的時候

id mockManager = [OCMockObject mockForClass:[ArticleManager class]];
[ArticleManager setSharedInstance:mockManager]; //把instance換成我們的mock
/*
在這裡做測試
*/
[ArticleManager setSharedInstance:nil]; //還原

這樣就可以使用自己的mock了。

沒有留言:

張貼留言