Singletion設計模式在cocoa中被廣泛使用。在我們平時寫App程式碼時也經常會將一些工具類,管理類設計成Singletion。Signletion通過一個類方法返回一個唯一的例項,與我們平常通過例項化生成一個個例項的場景有所不同。如果我們要stub一個Singletion的類的例項方法,那麼這個Signletion的類初始化方法(eg:sharedMange())必須返回一個mock物件。因為只有mock物件才可以做stub操作。那麼我們應該如何mock我們的Singletion呢,我們通過下面的例子一步步分析解決這個問題。
Singleton場景
比如我有一個Singleton的類(DemoStatusManage),他有一個例項方法currentStatus會返回一個1-100的隨機數。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
@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文案上。
1 2 3 |
- (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物件.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@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物件了。
1 |
mock = OCMClassMock([DemoStatusManage class]); |
當然我們也可以讓mock返回一個PartialMock物件。
1 |
mock = OCMPartialMock([[DemoStatusManage alloc] init]); |
包裝優化
去掉拷貝的程式碼
你應該也發現了,這段程式碼我們是拷貝過來的。
1 2 3 4 5 6 |
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差不多就長這樣。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@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方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
@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都是大同小異的,那麼我們不如寫個巨集定義來簡化我們的程式碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
#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了,來一個寫一行,來一雙寫兩行。
1 |
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
,則使用以下巨集在你重寫的方法前後做個包裝即可
1 2 3 4 5 6 |
#pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation" JTKMOCK_SINGLETON(DemoStatusManage,sharedManage) #pragma clang diagnostic pop |