
Singletion設計模式在cocoa中被廣泛使用。在我們平時寫App代碼時也經常會將一些工具類,管理類設計成Singletion。Signletion通過一個類方法返回一個唯一的實例,與我們平常通過實例化生成一個個實例的場景有所不同。如果我們要stub一個Singletion的類的實例方法,那麼這個Signletion的類初始化方法(eg:sharedMange())必須返回一個mock對象。因為只有mock對象才可以做stub操作。那麼我們應該如何mock我們的Singletion呢,我們通過下面的例子一步步分析解決這個問題。
Singleton場景
比如我有一個Singleton的類(DemoStatusManage),他有一個實例方法currentStatus會返回一個1-100的隨機數。
@interface DemoStatusManage : NSObject
+ (instancetype)sharedManage;
- (int)currentStatus;
@end
@implementation DemoStatusManage {
NSInteger _status;
}
+ (instancetype)sharedManage {
static dispatch_once_t once;
static DemoStatusManage *manage;
dispatch_once(&once, ^{
manage = [[DemoStatusManage alloc] init];
});
return manage;
}
- (instancetype)init {
self = [super init];
if (self) {
_status = 0;
}
return self;
}
- (int)currentStatus {
return [self getRandomNumber:1 to:100];
}
-(int)getRandomNumber:(int)from to:(int)to {
return (int)(from + (arc4random() % (to - from + 1)));
}
@end然後在我的另外一個類中會去調用這個Singletion的currentStatus方法,並且將返回的數據渲染到另外那個類的label文案上。
- (void)updateStatusNumber {
self.statusLabel.text = [NSString stringWithFormat:@"%ld",(long)[[DemoStatusManage sharedManage] currentStatus]];
}這是一個很簡單的Singletion場景,但是在測試updateStatusNumber這個API的時候由於依賴到了外部的DemoStatusManage的currentStatus方法,而且這個方法返回的是一個隨機數值,所以我們必須mock掉Singletion,然後再stub調currentStatus方法,讓這個方法返回我們期望的一個固定值。
應該用OCMock的哪個API呢
應該用OCMock的哪個API呢?OCMStrictClassMock(cls)? OCMClassMock(cls)? OCMPartialMock(obj)?
其實這裡按照常規的mock測試一個API都用不上。因為我們mock出來的東西(對象或者是類)只能在我們的測試用例中,updateStatusNumber方法裡面調用的永遠是DemoStatusManage的原生類。
那如何才能讓sharedManage不管在哪裡(測試用例中和updateStatusNumber中)都返回我們的mock對象呢,答案是用category重寫sharedManage讓它返回我們的mock對象.
@interface DemoStatusManage (UnitTest)
@end
static DemoStatusManage *mock = nil;
@implementation DemoStatusManage (UnitTest)
+ (instancetype)sharedManage {
if (mock) return mock;
static dispatch_once_t once;
static DemoStatusManage *manage;
dispatch_once(&once, ^{
manage = [[DemoStatusManage alloc] init];
});
return manage;
}
@end這樣在我們的單元測試類中只要在測試case中初始化一下mock,sharedManage不管在哪裡調用就都會返回我們需要的mock對象了。
mock = OCMClassMock([DemoStatusManage class]);
當然我們也可以讓mock返回一個PartialMock對象。
mock = OCMPartialMock([[DemoStatusManage alloc] init]);
包裝優化
去掉拷貝的代碼
你應該也發現了,這段代碼我們是拷貝過來的。
static dispatch_once_t once;
static DemoStatusManage *manage;
dispatch_once(&once, ^{
manage = [[DemoStatusManage alloc] init];
});
return manage;如果用這種方式,我們會陷入一個問題,我們在維護兩套相同的代碼,那天app工程中相關的sharedManage的方法有所變動,這裡也要相應的變動。有什麼辦法可以讓它找到原來的IMP實現呢,Matt大神的一篇文章中就告訴我們,Yes,可以的!Supersequent implementation.我們可以用Matt的invokeSupersequentNoArgs()宏定義來實現這個功能。
這樣我們的Cagegory差不多就長這樣。
@interface DemoStatusManage (UnitTest)
@end
static DemoStatusManage *mock = nil;
@implementation DemoStatusManage (UnitTest)
+ (instancetype)sharedManage {
if (mock) return mock;
return invokeSupersequentNoArgs()
}
@end包裝mock方法
筆者在用這種方式寫測試用例的時候發現,可能我的UnitTest這個Category是寫在Atest.m中的,但是在沒有寫Category也沒有引用Atest.m的Btest.m中,也會進入到重寫的sharedManage中,而由於mock是static的,也沒有做釋放操作,導致DemoStatusManage永遠是一個mock對象。可能是因為XCTest框架的原因,因為所有的XCTestCase都是沒有.h文件的,具體原因也不得而知。
所以,要解決這個問題,我們必須在mock使用完畢後釋放它,並且將創建和釋放都包裝出來,提供接口給測試用例調用。而且我們可以提供不同類型的mock方式。
@interface DemoStatusManage (UnitTest)
+ (instancetype)JTKCreateClassMock;
+ (instancetype)JTKCreatePartialMock:(DemoStatusManage *)obj;
+ (void)JTKReleaseMock;
@end
static DemoStatusManage *mock = nil;
@implementation DemoStatusManage (UnitTest)
+ (instancetype)sharedManage {
if (mock) return mock;
return invokeSupersequentNoArgs();
}
+ (instancetype)JTKCreateClassMock {
mock = OCMClassMock([DemoStatusManage class]);
return mock;
}
+ (instancetype)JTKCreatePartialMock:(DemoStatusManage *)obj {
mock = OCMPartialMock(obj);
return mock;
}
+ (void)JTKReleaseMock {
mock = nil;
}
@end這樣我們就可以在使用mock的時候調用JTKCreateClassMock 或者 JTKCreatePartialMock: 來生成我們需要的mock對象,在使用完畢後釋放我們的mock對象,就能實現我們的測試需求了。
宏定義簡化代碼
我們的工程中不可能只有一個Singletion,少則十幾,多則上百。如果我們對每個Singletion都這麼寫一遍Category的話,這個成本也太他媽大了。而其實不管是哪個Singletion,這個UnitTest的Category都是大同小異的,那麼我們不如寫個宏定義來簡化我們的代碼。
#define JTKMOCK_SINGLETON(__className,__sharedMethod) \
JTKMOCK_SINGLETON_CATEGORY_DECLARE(__className) \
JTKMOCK_SINGLETON_CATEGORY_IMPLEMENT(__className,__sharedMethod) \
#define JTKMOCK_SINGLETON_CATEGORY_DECLARE(__className) \
\
@interface __className (UnitTest) \
\
+ (instancetype)JTKCreateClassMock; \
\
+ (instancetype)JTKCreatePartialMock:(__className *)obj; \
\
+ (void)JTKReleaseMock; \
\
@end
#define JTKMOCK_SINGLETON_CATEGORY_IMPLEMENT(__className,__sharedMethod) \
\
static __className *mock_singleton_##__className = nil; \
\
@implementation __className (UnitTest) \
\
+ (instancetype)__sharedMethod { \
if (mock_singleton_##__className) return mock_singleton_##__className; \
return JTKInvokeSupersequentNoParameters(); \
} \
+ (instancetype)JTKCreateClassMock { \
mock_singleton_##__className = OCMClassMock([__className class]); \
return mock_singleton_##__className; \
} \
\
+ (instancetype)JTKCreatePartialMock:(__className *)obj { \
mock_singleton_##__className = OCMPartialMock(obj); \
return mock_singleton_##__className; \
} \
\
+ (void)JTKReleaseMock { \
mock_singleton_##__className = nil; \
} \
\
@end這樣我們只需要一行代碼就能搞定一個Singletion的UnitTest的Category了,來一個寫一行,來一雙寫兩行。
JTKMOCK_SINGLETON(DemoStatusManage,sharedManage)
One more thing
Matt文中代碼可以在github上找到NSObject+SupersequentImplementation
如果使用invokeSupersequentNoArgs()提示Too many arguments to function call,expected 0,have 2,請打開你的測試工程的target,找到Build Setting下的Enable Strict Checking of objc_mesSend Calls,設置為NO
用category重寫主類中的方法會有一個警告:Category is implementing a method which will also be implemented by its primary class,則使用以下宏在你重寫的方法前後做個包裝即可
#pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation" JTKMOCK_SINGLETON(DemoStatusManage,sharedManage) #pragma clang diagnostic pop