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