Singletion設(shè)計(jì)模式在cocoa中被廣泛使用。在我們平時(shí)寫App代碼時(shí)也經(jīng)常會(huì)將一些工具類,管理類設(shè)計(jì)成Singletion锣尉。Signletion通過一個(gè)類方法返回一個(gè)唯一的實(shí)例,與我們平常通過實(shí)例化生成一個(gè)個(gè)實(shí)例的場(chǎng)景有所不同。如果我們要stub一個(gè)Singletion的類的實(shí)例方法更耻,那么這個(gè)Signletion的類初始化方法(eg:sharedMange())必須返回一個(gè)mock對(duì)象。因?yàn)橹挥衜ock對(duì)象才可以做stub操作捏膨。那么我們應(yīng)該如何mock我們的Singletion呢秧均,我們通過下面的例子一步步分析解決這個(gè)問題。
Singleton場(chǎng)景
比如我有一個(gè)Singleton的類(DemoStatusManage),他有一個(gè)實(shí)例方法currentStatus會(huì)返回一個(gè)1-100的隨機(jī)數(shù)号涯。
123456789101112131415161718192021222324252627282930313233343536
@interfaceDemoStatusManage:NSObject+(instancetype)sharedManage;-(int)currentStatus;@end@implementationDemoStatusManage{NSInteger_status;}+(instancetype)sharedManage{staticdispatch_once_tonce;staticDemoStatusManage*manage;dispatch_once(&once,^{manage=[[DemoStatusManagealloc]init];});returnmanage;}-(instancetype)init{self=[superinit];if(self){_status=0;}returnself;}-(int)currentStatus{return[selfgetRandomNumber:1to:100];}-(int)getRandomNumber:(int)fromto:(int)to{return(int)(from+(arc4random()%(to-from+1)));}@end
然后在我的另外一個(gè)類中會(huì)去調(diào)用這個(gè)Singletion的currentStatus方法目胡,并且將返回的數(shù)據(jù)渲染到另外那個(gè)類的label文案上。
123
-(void)updateStatusNumber{self.statusLabel.text=[NSStringstringWithFormat:@"%ld",(long)[[DemoStatusManagesharedManage]currentStatus]];}
這是一個(gè)很簡(jiǎn)單的Singletion場(chǎng)景链快,但是在測(cè)試updateStatusNumber這個(gè)API的時(shí)候由于依賴到了外部的DemoStatusManage的currentStatus方法誉己,而且這個(gè)方法返回的是一個(gè)隨機(jī)數(shù)值,所以我們必須mock掉Singletion域蜗,然后再stub調(diào)currentStatus方法巨双,讓這個(gè)方法返回我們期望的一個(gè)固定值。
應(yīng)該用OCMock的哪個(gè)API呢
應(yīng)該用OCMock的哪個(gè)API呢霉祸?OCMStrictClassMock(cls)? OCMClassMock(cls)? OCMPartialMock(obj)?
其實(shí)這里按照常規(guī)的mock測(cè)試一個(gè)API都用不上筑累。因?yàn)槲覀僲ock出來的東西(對(duì)象或者是類)只能在我們的測(cè)試用例中,updateStatusNumber方法里面調(diào)用的永遠(yuǎn)是DemoStatusManage的原生類丝蹭。
那如何才能讓sharedManage不管在哪里(測(cè)試用例中和updateStatusNumber中)都返回我們的mock對(duì)象呢慢宗,答案是用category重寫sharedManage讓它返回我們的mock對(duì)象.
12345678910111213141516171819
@interfaceDemoStatusManage(UnitTest)@endstaticDemoStatusManage*mock=nil;@implementationDemoStatusManage(UnitTest)+(instancetype)sharedManage{if(mock)returnmock;staticdispatch_once_tonce;staticDemoStatusManage*manage;dispatch_once(&once,^{manage=[[DemoStatusManagealloc]init];});returnmanage;}@end
這樣在我們的單元測(cè)試類中只要在測(cè)試case中初始化一下mock,sharedManage不管在哪里調(diào)用就都會(huì)返回我們需要的mock對(duì)象了。
1
mock=OCMClassMock([DemoStatusManageclass]);
當(dāng)然我們也可以讓mock返回一個(gè)PartialMock對(duì)象婆廊。
1
mock=OCMPartialMock([[DemoStatusManagealloc]init]);
包裝優(yōu)化
去掉拷貝的代碼
你應(yīng)該也發(fā)現(xiàn)了迅细,這段代碼我們是拷貝過來的。
123456
staticdispatch_once_tonce;staticDemoStatusManage*manage;dispatch_once(&once,^{manage=[[DemoStatusManagealloc]init];});returnmanage;
如果用這種方式淘邻,我們會(huì)陷入一個(gè)問題茵典,我們?cè)诰S護(hù)兩套相同的代碼,那天app工程中相關(guān)的sharedManage的方法有所變動(dòng)宾舅,這里也要相應(yīng)的變動(dòng)统阿。有什么辦法可以讓它找到原來的IMP實(shí)現(xiàn)呢,Matt大神的一篇文章中就告訴我們筹我,Yes扶平,可以的!Supersequent implementation.我們可以用Matt的invokeSupersequentNoArgs()宏定義來實(shí)現(xiàn)這個(gè)功能蔬蕊。
這樣我們的Cagegory差不多就長(zhǎng)這樣结澄。
12345678910111213
@interfaceDemoStatusManage(UnitTest)@endstaticDemoStatusManage*mock=nil;@implementationDemoStatusManage(UnitTest)+(instancetype)sharedManage{if(mock)returnmock;returninvokeSupersequentNoArgs()}@end
包裝mock方法
筆者在用這種方式寫測(cè)試用例的時(shí)候發(fā)現(xiàn),可能我的UnitTest這個(gè)Category是寫在Atest.m中的岸夯,但是在沒有寫Category也沒有引用Atest.m的Btest.m中麻献,也會(huì)進(jìn)入到重寫的sharedManage中,而由于mock是static的猜扮,也沒有做釋放操作勉吻,導(dǎo)致DemoStatusManage永遠(yuǎn)是一個(gè)mock對(duì)象÷糜可能是因?yàn)閄CTest框架的原因齿桃,因?yàn)樗械腦CTestCase都是沒有.h文件的,具體原因也不得而知煮盼。
所以短纵,要解決這個(gè)問題,我們必須在mock使用完畢后釋放它僵控,并且將創(chuàng)建和釋放都包裝出來踩娘,提供接口給測(cè)試用例調(diào)用。而且我們可以提供不同類型的mock方式喉祭。
12345678910111213141516171819202122232425262728293031323334
@interfaceDemoStatusManage(UnitTest)+(instancetype)JTKCreateClassMock;+(instancetype)JTKCreatePartialMock:(DemoStatusManage*)obj;+(void)JTKReleaseMock;@endstaticDemoStatusManage*mock=nil;@implementationDemoStatusManage(UnitTest)+(instancetype)sharedManage{if(mock)returnmock;returninvokeSupersequentNoArgs();}+(instancetype)JTKCreateClassMock{mock=OCMClassMock([DemoStatusManageclass]);returnmock;}+(instancetype)JTKCreatePartialMock:(DemoStatusManage*)obj{mock=OCMPartialMock(obj);returnmock;}+(void)JTKReleaseMock{mock=nil;}@end
這樣我們就可以在使用mock的時(shí)候調(diào)用JTKCreateClassMock 或者 JTKCreatePartialMock: 來生成我們需要的mock對(duì)象养渴,在使用完畢后釋放我們的mock對(duì)象,就能實(shí)現(xiàn)我們的測(cè)試需求了泛烙。
宏定義簡(jiǎn)化代碼
我們的工程中不可能只有一個(gè)Singletion理卑,少則十幾,多則上百蔽氨。如果我們對(duì)每個(gè)Singletion都這么寫一遍Category的話藐唠,這個(gè)成本也太他媽大了帆疟。而其實(shí)不管是哪個(gè)Singletion,這個(gè)UnitTest的Category都是大同小異的宇立,那么我們不如寫個(gè)宏定義來簡(jiǎn)化我們的代碼踪宠。
12345678910111213141516171819202122232425262728293031323334353637383940414243
#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
這樣我們只需要一行代碼就能搞定一個(gè)Singletion的UnitTest的Category了,來一個(gè)寫一行妈嘹,來一雙寫兩行柳琢。
1
JTKMOCK_SINGLETON(DemoStatusManage,sharedManage)
One more thing
Matt文中代碼可以在github上找到NSObject+SupersequentImplementation
如果使用invokeSupersequentNoArgs()提示Too many arguments to function call,expected 0,have 2,請(qǐng)打開你的測(cè)試工程的target,找到Build Setting下的Enable Strict Checking of objc_mesSend Calls,設(shè)置為NO
用category重寫主類中的方法會(huì)有一個(gè)警告:Category is implementing a method which will also be implemented by its primary class,則使用以下宏在你重寫的方法前后做個(gè)包裝即可
123456
#pragma clang diagnostic push#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"JTKMOCK_SINGLETON(DemoStatusManage,sharedManage)#pragma clang diagnostic pop