單元測試和OCMock

OCMock使用
一磷蛹、安裝及簡單使用:
使用Cocoapod引入:
pod 'OCMock'


image.png

簡單使用:
新建一個單元測試文件


image.png

image.png

引入OCMock


image.png

新建一個Person類,添加連個屬性name、age


image.png

新建一個簡單的單元測試方法


image.png

Mock出一個人圆兵,sub getAge方法返回值為10归形;新建一個人對象,給他的年齡設置為10左胞,我們設置一個斷言,我們mock出的人的年齡和新建人對象的年齡相等举户,Test Success

二烤宙、OCMock作用及實現(xiàn)原理
由上面我們可以看出,對于一些不容易構(gòu)造或不容易獲取的對象俭嘁,此時你可以創(chuàng)建一個虛擬的對象來完成測試躺枕。
實現(xiàn)思想:根據(jù)要mock的對象的class來創(chuàng)建一個對應的對象,并且設置好該對象的屬性和調(diào)用預定方法后的動作(例如返回一個值供填,調(diào)用代碼塊拐云,發(fā)送消息等等),然后將其記錄到一個數(shù)組中近她,接下來開發(fā)者主動調(diào)用該方法叉瘩,最后做一個verify(驗證),從而判斷該方法是否被調(diào)用粘捎,或者調(diào)用過程中是否拋出異常等房揭。

三、使用說明
1.創(chuàng)建Mock對象
1.1 類Mock
id classMock = OCMClassMock([SomeClass class]);

1.2 協(xié)議Mock
id protocolMock = OCMProtocolMock(@protocol(SomeProtocol));

1.3 嚴格的類和協(xié)議Mock
默認的mock方式是nice(方法調(diào)用的時候返回nil或者是返回正確的方法)
嚴格的模式下,mock的對象在調(diào)用沒有被stub(置換)的方法的時候,會拋出異常.
id classMock = OCMStrictClassMock([SomeClass class]);id protocolMock = OCMStrictProtocolMock(@protocol(SomeProtocol));

1.4 部分Mock
id partialMock = OCMPartialMock(anObject)
這樣創(chuàng)建的對象在調(diào)用方法時:
如果方法被stub,調(diào)用stub后的方法.
如果方法沒有被stub,調(diào)用原來的對象的方法.
partialMock 對象在調(diào)用方法后,可以用于稍后的驗證此方法的調(diào)用情況(被調(diào)用,調(diào)用結(jié)果)

1.5 觀察者Mock
id observerMock = OCMObserverMock();
這樣創(chuàng)建的對象可以用于觀察/通知.

2 置換方法
2.1 置換方法(待置換的方法返回objects)
OCMStub([mock someMethod]).andReturn(anObject);
在mock對象上調(diào)用某個方法的時候,這個方法一定返回一個anObject.(也就是說強制替換了某個方法的返回值為anObject)

2.2 置換方法(待置換的方法返回values)
OCMStub([mock aMethodReturningABoolean]).andReturn(YES);
在mock對象上調(diào)用某個方法的時候,這個方法一定返回values.
注意這里的原始值類型一定要和原來的方法的返回值一致.

2.3 委托到另一個方法(置換委托方法到另外一個方法)
OCMStub([mock someMethod]).andCall(anotherObject, @selector(aDifferentMethod));
置換mock 對象的someMethod ==> anotherObject 的aDifferentMethod.
這樣,當mock對象調(diào)用someMethod方法的時候,實際上的操作就是anotherObject 調(diào)用了aDifferentMethod方法.

2.4 置換一個blcok方法.
OCMStub([mock someMethod]).andDo(^(NSInvocation invocation) { / block that handles the method invocation */ });
在mock對象調(diào)用someMethod的時候,andDo后面的block會調(diào)用.block可以從NSInvocation中得到一些參數(shù),然后使用這個NSInvocation對象來構(gòu)造返回值等等.

2.5 置換方法的參數(shù)
OCMStub([mock someMethodWithReferenceArgument:[OCMArg setTo:anObject]]);
OCMStub([mock someMethodWithReferenceArgument:[OCMArg setToValue:OCMOCK_VALUE((int){aValue})]]);
mock對象在調(diào)用某個帶參數(shù)的方法的時候,這個方法的參數(shù)可以被置換.
setTo用來設置對象參數(shù),setToValue用來設置原始值類型的參數(shù).

2.6 調(diào)用某個方法就拋出異常
OCMStub([mock someMethod]).andThrow(anException);
當mock對象調(diào)用someMethod的時候,就會拋出異常

2.7 調(diào)用某個方法就發(fā)送通知
OCMStub([mock someMethod]).andPost(aNotification);
當mock對象調(diào)用someMethod的時候,就會發(fā)送通知.

2.8 鏈式調(diào)用
OCMStub([mock someMethod]).andPost(aNotification).andReturn(aValue);
所有的actions(比如andReturn,andPost)可以鏈式調(diào)用.上面的例子中,mock對象調(diào)用someMethod方法后,發(fā)送通知,返回aValue

2.9 轉(zhuǎn)發(fā)的原來的對象/類
OCMStub([mock someMethod]).andForwardToRealObject();
使用部分mock的時候,使用類方法的可以轉(zhuǎn)發(fā)到原來的對象/原來的類.
這個功能在鏈式調(diào)用或者是使用expectation的時候很有用.

2.10 什么也不做
OCMStub([mock someMethod]).andDo(nil);
可以給andDo傳入nil參數(shù),而不是原來一個block作為參數(shù).
這個功能在使用部分mock/mock類的時候很有用,可以屏蔽原來的行為.

3 驗證作用
3.1 運行后就驗證
id mock = OCMClassMock([SomeClass class]);
/* run code under test */
OCMVerify([mock someMethod]);
在mock對象調(diào)用someMethod后就開始驗證.(如果這個方法沒有被調(diào)用),就拋出一個錯誤.
在驗證語句中可以使用 參數(shù)約束.

3.2 置換后驗證
id mock = OCMClassMock([SomeClass class]);
OCMStub([mock someMethod]).andReturn(myValue);
/* run code under test */
OCMVerify([mock someMethod]);
在置換某個方法(置換了返回的參數(shù))后,然后可以驗證這個方法是否被調(diào)用.

4 參數(shù)約束
4.1 任意參數(shù)約束
OCMStub([mock someMethodWithAnArgument:[OCMArg any]])
OCMStub([mock someMethodWithPointerArgument:[OCMArg anyPointer]])
OCMStub([mock someMethodWithSelectorArgument:[OCMArg anySelector]])
不管傳遞什么參數(shù),對于所有活躍的invocations,置換該方法.Pointers 和selectors 需要像上面一樣特殊對待.對于既不是對象,也不是指針,更不是SEL類型的,不可以忽略的參數(shù),可以使用 any 來代替.

4.2 忽略非對象的參數(shù)
[[[mock stub] ignoringNonObjectArgs] someMethodWithIntArgument:0]
在這個invocation中,mock忽略所有的非對象參數(shù).mock對象將會接收所有的someMethodWithIntArgument 方法 invocation,而不去管實際傳遞進來的參數(shù)是什么.如果這個方法含有對象參數(shù)和非對象參數(shù),對象參數(shù)仍然可以使用OCMArg的參數(shù)約束.

4.3 匹配參數(shù)
OCMStub([mock someMethod:aValue)
OCMStub([mock someMethod:[OCMArg isNil]])
OCMStub([mock someMethod:[OCMArg isNotNil]])
OCMStub([mock someMethod:[OCMArg isNotEqual:aValue]])
OCMStub([mock someMethod:[OCMArg isKindOfClass:[SomeClass class]]])
OCMStub([mock someMethod:[OCMArg checkWithSelector:aSelector onObject:anObject]])
OCMStub([mock someMethod:[OCMArg checkWithBlock:^BOOL(id value) { /* return YES if value is ok */ }]])
如果在置換創(chuàng)建的時候,有個一個參數(shù)傳遞進來了,置換方法將僅僅匹配精確參數(shù)的invocations.帶不同的參數(shù)來調(diào)用的方法不會被匹配.
OCMArg類提供了幾個不同的方法來匹配不同的參數(shù)類型.
對于checkWithSelector:onObject:方法, 當mock對象接收到someMethod:的時候, 會觸發(fā) anObject上的aSelector方法. 如果方法帶參數(shù),這個參數(shù)會傳遞給someMethod:. 這個方法應該返回一個BOOL值,表示這個參數(shù)是否和預期的一樣.

4.4 使用Hamcrest來匹配
OCMStub([mock someMethod:startsWith(@"foo")]
OCMock不帶 Hamcrest 框架,所以如果想要使用的話,需要自己安裝Hamcrest .

5 類方法的Mock
5.1 置換類方法
id classMock = OCMClassMock([SomeClass class]);
OCMStub([classMock aClassMethod]).andReturn(@"Test string");
// result is @"Test string"
NSString *result = [SomeClass aClassMethod];
置換類方法和置換實例方法的步驟相像.但是mock對象在深層次上對原有 類做了些更改.(替換了原有的的類的meta class).這讓置換調(diào)用直接作用在mock對象上,而不是原有的類.
注意:
添加到類方法上的mock對象跨越了多個測試,mock的類對象在置換后不會deallocated,需要手動來取消這個mock關系.
如果mock對象作用于同一個類, 這時的行為就不預測了.

5.2 驗證類方法的調(diào)用
id classMock = OCMClassMock([SomeClass class]);
/* run code under test */
OCMVerify([classMock aClassMethod]);
驗證類方法的調(diào)用和驗證實例方法的調(diào)用的使用方式一樣.

5.3 有歧義的類方法和實例方法
id classMock = OCMClassMock([SomeClass class]);
OCMStub(ClassMethod([classMock ambiguousMethod])).andReturn(@"Test string");
// result is @"Test string"
NSString *result = [SomeClass ambiguousMethod];
置換了類方法,但是類有一個和類方法同名的實例方法,置換類方法的時候,必須使用ClassMethod()

5.4 恢復類
id classMock = OCMClassMock([SomeClass class]);
/* do stuff */
[classMock stopMocking];
置換類方法后,可以將類恢復到原來的狀態(tài),通過調(diào)用stopMocking來完成.
如果在結(jié)束測試前,需要恢復到原來的狀態(tài)的話,這就很有用了.
在mock對象被釋放的時候,stopMocking會自動調(diào)用.
當類恢復到原來的對象,類對象的meta class會變?yōu)樵瓉淼膍eta class.這會移除所有的方法置換.
在調(diào)用了stopMocking之后,不應該繼續(xù)使用mock對象.

6 部分Mock
6.1 置換方法
id partialMock = OCMPartialMock(anObject);
OCMStub([partialMock someMethod]).andReturn(@"Test string");
// result1 is @"Test string"
NSString *result1 = [partialMock someMethod];
// result2 is @"Test string", too!
NSString *result2 = [anObject someMethod];
部分Mock修改了原有的mock對象的類.(實際上是繼承了待mock對象,然后替換用 繼承的類來代替原有的類).
這就是說: 使用真實的對象來調(diào)用,即使是使用self,也會影響 置換方法和預期的結(jié)果.

6.2 驗證方法調(diào)用
id partialMock = OCMPartialMock(anObject);
/* run code under test */
OCMVerify([partialMock someMethod]);
驗證方法的調(diào)用和驗證類方法,驗證協(xié)議的調(diào)用類似.

6.3 恢復對象
id partialMock = OCMPartialMock(anObject);
/* do stuff */
[partialMock stopMocking];
真正的對象可以通過調(diào)用stopMocking方法來恢復到原來的狀態(tài).
這種情況只有在結(jié)束測試之前需要恢復到原來狀態(tài).
部分mock對象會在釋放的時候,會自動調(diào)用 stopMocking方法.
當對象轉(zhuǎn)變?yōu)樵瓉淼臓顟B(tài)后,類會變?yōu)樵瓉淼念?也會移除所有的置換方法.
在調(diào)用了stopMocking之后,最好不要去使用mock對象.

7 嚴格mock和期望
7.1 Expect-run-verify 期望-運行-驗證
id classMock = OCMClassMock([SomeClass class]);
OCMExpect([classMock someMethodWithArgument:[OCMArg isNotNil]]);
/* run code under test, which is assumed to call someMethod */
OCMVerifyAll(classMock)
這是使用mock最原始的方法:
創(chuàng)建mock對象
期望調(diào)用某個方法
測試代碼(預想的是這段測試代碼會調(diào)用上面期望調(diào)用的方法.
驗證mock對象(也就是驗證期望的方法是否被調(diào)用了)
如果預期的方法沒有被調(diào)用,或者調(diào)用的時候,傳遞的參數(shù)不對,那么就好產(chǎn)生錯誤.可以使用上面 參數(shù)約束.
嚴格的mock可以用在類和協(xié)議上.
如果有懷疑的話,可以使用 3 驗證作用

7.2 嚴格的mock 和快速失敗
id classMock = OCMStrictClassMock([SomeClass class]);
[classMock someMethod]; // this will throw an exception
上面mock沒有設置任何期望,直接掉調(diào)用某個方法會拋出異常.
當超出去預期的調(diào)用的時候,會立即測試失敗. 只有strict mock才會快速失敗.

7.3 置換操作和預期
id classMock = OCMStrictClassMock([SomeClass class]);
OCMExpect([classMock someMethod]).andReturn(@"a string for testing");
/* run code under test, which is assumed to call someMethod */
OCMVerifyAll(classMock)
可以使用andReturn,andThrow,等預期的操作.如果方法被調(diào)用,會調(diào)用置換 方法,確認方法確實被調(diào)用了.

7.4 延時驗證
id mock = OCMStrictClassMock([SomeClass class]);
OCMExpect([mock someMethod]);
/* run code under test, which is assumed to call someMethod eventually */
OCMVerifyAllWithDelay(mock, aDelay);
在某種情況下,預期的方法只有在 run loop 出于活躍狀態(tài)的時候才會被調(diào)用.這時,可以將認證延時一會.aDelay是mock對象會等待的最大時間.通常情況下,在預期達到后就會返回.

7.5 依序驗證
id mock = OCMStrictClassMock([SomeClass class]);
[mock setExpectationOrderMatters:YES];
OCMExpect([mock someMethod]);
OCMExpect([mock anotherMethod]);
// calling anotherMethod before someMethod will cause an exception to be thrown
[mock anotherMethod];
mock會按照在預期中設置好的順序來判斷.只要調(diào)用的不是按照期望的調(diào)用順序,這個時候就會拋出異常.

8 觀察者mock
8.1 準備工作
id observerMock = OCMObserverMock();
[notificatonCenter addMockObserver:aMock name:SomeNotification object:nil];
[[mock expect] notificationWithName:SomeNotification object:[OCMArg any]];
為觀察者和通知創(chuàng)建一個mock對象.
在通知中心注冊對象
預期會調(diào)用這個通知.

8.2 驗證
OCMVerifyAll(observerMock);
目前觀察者 mock 總是嚴格的mock.當一個不在預期中的通知調(diào)用的時候,就會拋出一個異常.
這就是說,單個的通知實際上不是能被驗證的.所有的通知必須按照預期賴設置.他們會在通過調(diào)用OCMVerifyAll來一起驗證.

9 進階話題
9.1 對于普通的mock,快速失敗
對strict mock 對象,在一個mock對象上調(diào)用沒有被mock方法(沒有被置換)的時候,會拋出一個異常,這時候會發(fā)生 快速失敗.
id mock = OCMClassMock([SomeClass class]);
[[mock reject] someMethod];
這種情況下,mock會接受除了someMethod 的所有方法.觸發(fā)someMethod方法會導致快速失敗.

9.2 在OCMVerifyAll時重新拋出異常
在fail-fast的時候會拋出異常,但是這并不一定會導致測試失敗.
通過調(diào)用OCMVerifyAll重新拋出異成味耍可以導致測試失敗.
這個功能在不在預期中的從notifications引發(fā)的invocations出現(xiàn)的時候使用.

9.3 置換創(chuàng)建對象的方法
id classMock = OCMClassMock([SomeClass class]);
OCMStub([classMock copy])).andReturn(myObject);
可以置換創(chuàng)建對象的 類/實例方法.當被置換的方法以 alloc,new,copy,mutableCopy開頭的方法時,OCMock會自動調(diào)整對象的引用計數(shù).
id classMock = OCMClassMock([SomeClass class]);
OCMStub([classMock new])).andReturn(myObject);
盡管可以置換類的new方法,但是不建議這么做.
沒有辦法置換 init 方法,因為這個方法是被mock對象自己實現(xiàn)的.

9.4 基于實例對象的方法替換
id partialMock = OCMPartialMock(anObject);
OCMStub([partialMock someMethod]).andCall(differentObject, @selector(differentMethod));
用一句話概括起來,Method swizzling 會在運行時替換一個方法的實現(xiàn).
使用 partial mock然后調(diào)用 andCall操作可以實現(xiàn)這個方法替換.
當anObject收到someMethod消息時,anObject的實現(xiàn)沒有觸發(fā),相反的,
differentObject的differentMethod得到調(diào)用.
其他方法并不會收到影響,仍然會調(diào)用原來的的方法的實現(xiàn).

10 使用限制
10.1 在一個指定的類上,只能有一個mock對象
// don't do this
id mock1 = OCMClassMock([SomeClass class]);
OCMStub([mock1 aClassMethod]);
id mock2 = OCMClassMock([SomeClass class]);
OCMStub([mock2 anotherClassMethod]);
原因是類的meta class 替換后,不會釋放,mock類仍會存在,甚至可能跨tests.
如果多個相同mock對象管理同一個類,運行時的行為就不可確定.

10.2 在被置換的方法上設置期望,會不起作用
id mock = OCMStrictClassMock([SomeClass class]);
OCMStub([mock someMethod]).andReturn(@"a string");
OCMExpect([mock someMethod]);
/* run code under test */
OCMVerifyAll(mock); // will complain that someMethod has not been called
上面代碼先替換了someMethod,然后強制someMethod返回”a string"
由于現(xiàn)在mock的實現(xiàn),所有的someMethod都會置換所處理.所以,即使這個方法被調(diào)用,這個驗證也會失敗.
可以通過在expect后添加andReturn來避免這個問題. 也可以通過在expect后再次設置一個方法替換.

10.3 Partial mock 不能在某些特定的類使用
id partialMockForString = OCMPartialMock(@"Foo");
// will throw an exception
NSDate *date = [NSDate dateWithTimeIntervalSince1970:0];
id partialMockForDate = OCMPartialMock(date);
// will throw on some architectures
不可能創(chuàng)建一個 toll-free bridged的類,例如 NSString,或者是NSDate.
如果你試圖這么去做,那么可能會拋出一個異常.

10.4 某些特定的類不能被置換和驗證
id partialMockForString = OCMPartialMock(anObject);
OCMStub([partialMock class]).andReturn(someOtherClass); // will not work
不能mock某些運行時的方法,例如
class,
methodSignatureForSelector:
forwardInvocation:

10.5 NSString的類方法不能被置換和驗證
id stringMock = OCMClassMock([NSString class]);
// the following will not work
OCMStub([stringMock stringWithContentsOfFile:[OCMArg any] encoding:NSUTF8StringEncoding error:[OCMArg setTo:nil]]);

10.6 NSObject 的方法不能被驗證
id mock = OCMClassMock([NSObject class]);
/* run code under test, which calls awakeAfterUsingCoder: */
OCMVerify([mock awakeAfterUsingCoder:[OCMArg any]]);
// still fails
不可能在NSObject 和它的分類category上使用verify-after-running.
在某些情況下可能置換這個方法,然后驗證.

10.7 apple 的私有方法不能被驗證
UIWindow window = / get window somehow /
id mock = OCMPartialMock(window);
/
run code under test, which causes _sendTouchesForEvent: to be invoked */
OCMVerify([mock _sendTouchesForEvent:[OCMArg any]]);
// still fails
含有下劃線前綴,后綴,NS,UI開頭的方法等.

10.8 Verify-after-running不能使用延時
只有在 嚴格的mock和期望中,可以使用expect-run-verify

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末捅暴,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子咧纠,更是在濱河造成了極大的恐慌蓬痒,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件漆羔,死亡現(xiàn)場離奇詭異梧奢,居然都是意外死亡,警方通過查閱死者的電腦和手機演痒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進店門亲轨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人鸟顺,你說我怎么就攤上這事惦蚊∑飨海” “怎么了?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵蹦锋,是天一觀的道長兆沙。 經(jīng)常有香客問我,道長莉掂,這世上最難降的妖魔是什么葛圃? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮憎妙,結(jié)果婚禮上库正,老公的妹妹穿的比我還像新娘。我一直安慰自己厘唾,他們只是感情好褥符,可當我...
    茶點故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著阅嘶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪载迄。 梳的紋絲不亂的頭發(fā)上讯柔,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天,我揣著相機與錄音护昧,去河邊找鬼魂迄。 笑死,一個胖子當著我的面吹牛惋耙,可吹牛的內(nèi)容都是我干的捣炬。 我是一名探鬼主播,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼绽榛,長吁一口氣:“原來是場噩夢啊……” “哼湿酸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起灭美,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤推溃,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后届腐,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體铁坎,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年犁苏,在試婚紗的時候發(fā)現(xiàn)自己被綠了硬萍。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡围详,死狀恐怖朴乖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤寒砖,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布赐劣,位于F島的核電站,受9級特大地震影響哩都,放射性物質(zhì)發(fā)生泄漏魁兼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一漠嵌、第九天 我趴在偏房一處隱蔽的房頂上張望咐汞。 院中可真熱鬧,春花似錦儒鹿、人聲如沸化撕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽植阴。三九已至,卻和暖如春圾浅,著一層夾襖步出監(jiān)牢的瞬間掠手,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工狸捕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留喷鸽,地道東北人。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓灸拍,卻偏偏與公主長得像做祝,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子鸡岗,可洞房花燭夜當晚...
    茶點故事閱讀 43,658評論 2 350

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