[iOS單元測(cè)試系列]Singleton如何測(cè)試

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末润脸,一起剝皮案震驚了整個(gè)濱河市柬脸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌毙驯,老刑警劉巖倒堕,帶你破解...
    沈念sama閱讀 222,807評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異爆价,居然都是意外死亡垦巴,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門铭段,熙熙樓的掌柜王于貴愁眉苦臉地迎上來骤宣,“玉大人,你說我怎么就攤上這事稠项。” “怎么了鲜结?”我有些...
    開封第一講書人閱讀 169,589評(píng)論 0 363
  • 文/不壞的土叔 我叫張陵展运,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我精刷,道長(zhǎng)拗胜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,188評(píng)論 1 300
  • 正文 為了忘掉前任怒允,我火速辦了婚禮埂软,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘纫事。我一直安慰自己勘畔,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評(píng)論 6 398
  • 文/花漫 我一把揭開白布丽惶。 她就那樣靜靜地躺著炫七,像睡著了一般。 火紅的嫁衣襯著肌膚如雪钾唬。 梳的紋絲不亂的頭發(fā)上万哪,一...
    開封第一講書人閱讀 52,785評(píng)論 1 314
  • 那天侠驯,我揣著相機(jī)與錄音,去河邊找鬼奕巍。 笑死吟策,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的的止。 我是一名探鬼主播檩坚,決...
    沈念sama閱讀 41,220評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼冲杀!你這毒婦竟也來了效床?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,167評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤权谁,失蹤者是張志新(化名)和其女友劉穎剩檀,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體旺芽,經(jīng)...
    沈念sama閱讀 46,698評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡沪猴,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評(píng)論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了采章。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片运嗜。...
    茶點(diǎn)故事閱讀 40,912評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖悯舟,靈堂內(nèi)的尸體忽然破棺而出担租,到底是詐尸還是另有隱情,我是刑警寧澤抵怎,帶...
    沈念sama閱讀 36,572評(píng)論 5 351
  • 正文 年R本政府宣布奋救,位于F島的核電站,受9級(jí)特大地震影響反惕,放射性物質(zhì)發(fā)生泄漏尝艘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評(píng)論 3 336
  • 文/蒙蒙 一姿染、第九天 我趴在偏房一處隱蔽的房頂上張望背亥。 院中可真熱鬧,春花似錦悬赏、人聲如沸狡汉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽轴猎。三九已至,卻和暖如春进萄,著一層夾襖步出監(jiān)牢的瞬間捻脖,已是汗流浹背锐峭。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留可婶,地道東北人沿癞。 一個(gè)月前我還...
    沈念sama閱讀 49,359評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像矛渴,于是被迫代替她去往敵國(guó)和親椎扬。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • Singletion設(shè)計(jì)模式在cocoa中被廣泛使用具温。在我們平時(shí)寫App代碼時(shí)也經(jīng)常會(huì)將一些工具類蚕涤,管理類設(shè)計(jì)成S...
    子循_陳奕龍閱讀 830評(píng)論 0 2
  • Startup 單元測(cè)試的核心價(jià)值在于兩點(diǎn): 更加精確地定義某段代碼的作用,從而使代碼的耦合性更低 避免程序員寫出...
    wuwenxiang閱讀 10,109評(píng)論 1 27
  • Mock單例 單例模式是我們?cè)趇OS中最常使用的設(shè)計(jì)模式之一铣猩。單例模式不需要傳遞任何參數(shù)揖铜,它通過一個(gè)類方法返回一個(gè)...
    隨惢所欲閱讀 2,654評(píng)論 1 5
  • 終于把前面的base文件夾簡(jiǎn)簡(jiǎn)單單的看了一遍,終于可以回到正片上來了达皿,保證不爛尾天吓。 項(xiàng)目天天用yymodel解析數(shù)...
    充滿活力的早晨閱讀 1,375評(píng)論 1 0
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,732評(píng)論 0 9