一、OCMock簡(jiǎn)介
1.1、Mock介紹
作為一個(gè)動(dòng)詞,mock是模擬、模仿的意思;作為一個(gè)名詞,mock是能夠模仿真實(shí)對(duì)象行為的模擬對(duì)象。在軟件測(cè)試中,mock所模擬的對(duì)象是什么呢?它一定不是我們所測(cè)試的對(duì)象,而是 SUT(Software Under Test:測(cè)試的對(duì)象) 的依賴(lài)(dependency)。換句話(huà)說(shuō),mock 的作用是模擬 SUT 依賴(lài)對(duì)象的行為。
文字不好理解,我們畫(huà)個(gè)圖,如下圖所示,被測(cè)試對(duì)象是 A,A 依賴(lài)的是B,B 依賴(lài)的是 C。而我們要 mock 的是 B 的行為。圖中 A 就是 SUT。

1.2、OCMock介紹
OCMock是一個(gè)用于為iOS或Mac OS X項(xiàng)目配置Mock測(cè)試的開(kāi)源項(xiàng)目。
其實(shí)現(xiàn)思想就是根據(jù)要mock的對(duì)象的class來(lái)創(chuàng)建一個(gè)對(duì)應(yīng)的對(duì)象,并且設(shè)置好該對(duì)象的屬性和調(diào)用預(yù)定方法后的動(dòng)作(例如返回一個(gè)值,調(diào)用代碼塊,發(fā)送消息等等),然后將其記錄到一個(gè)數(shù)組中,接下來(lái)開(kāi)發(fā)者主動(dòng)調(diào)用該方法,最后做一個(gè)verify(驗(yàn)證),從而判斷該方法是否被調(diào)用,或者調(diào)用過(guò)程中是否拋出異常等。
其實(shí)就是可以把它當(dāng)做我們偽造的一個(gè)對(duì)象,我們給它一些預(yù)設(shè)的值之類(lèi)的,然后就可以進(jìn)行對(duì)應(yīng)的驗(yàn)證了。
1.3、OCMock集成
項(xiàng)目集成 OCMock 第三方庫(kù),這個(gè)使用 pod 工具直接安裝OCMock框架即可。若使用 iBiu 工具安裝 OCMock 庫(kù)需在 podfile 文件同級(jí)創(chuàng)建 Podfile.custom。
使用普通的 pod 文件相同格式添加 OCmock 如下:
source 'https://github.com/CocoaPods/Specs.git'
pod 'OCMock'
二、OCMock的入門(mén)
關(guān)于為什么需要 mock,OCMock 官網(wǎng)的 Introduction 舉了以下一個(gè)例子(是個(gè)標(biāo)準(zhǔn)的 TDD 開(kāi)發(fā)流程,值得學(xué)習(xí)一下):開(kāi)發(fā)者需要開(kāi)發(fā)一個(gè)從 Twitter 上拉取數(shù)據(jù),然后更新用戶(hù)界面的模塊,如何應(yīng)用 TDD 編寫(xiě)該模塊的單元測(cè)試。接下來(lái)的內(nèi)容,是根據(jù) TDD 流程劃分小節(jié),關(guān)于 mock 的存在價(jià)值則分散在每個(gè)小節(jié)各處。
點(diǎn)擊下載Demo:ZJHUnitTestDemo
2.1、示例模塊劃分
首先,劃分大致模塊,例如最簡(jiǎn)單的 MVC 模塊劃分方式,以確定接口。
Controller
@interface ZJHTwitterViewController : UIViewController
@property (nonatomic, strong) ZJHTwitterConnection *connection;
@property (nonatomic, strong) ZJHTweetView *tweetView;
- (void)updateTweetView;
@end
Data Source
@interface ZJHTwitterConnection : NSObject
// 檢索新推文的方法。它返回一個(gè)ZJHTweetModel對(duì)象數(shù)組,如果無(wú)法處理請(qǐng)求,則返回nil。
- (NSArray <ZJHTweetModel *> *)fetchTweets;
@end
View
@interface ZJHTweetView : UIView
// 一個(gè)將單個(gè)推文添加到視圖的方法
- (void)addTweet:(ZJHTweetModel *)aTweet;
@en
2.2、確定測(cè)試用例三要素
選定實(shí)現(xiàn)ZJHTwitterViewController的updateTweetView方法,該方法通過(guò)調(diào)用connection成員的fetchTweets獲取 Twitter 數(shù)據(jù),然后調(diào)用tweetView成員的addTweet:將數(shù)據(jù)顯示到界面。TDD(Test Driven Development:測(cè)試驅(qū)動(dòng)開(kāi)發(fā)) 是測(cè)試先行,因此先編寫(xiě)針對(duì)updateTweetView方法的單元測(cè)試。在此之前,需要考慮如何處理Controller對(duì)View和Connection的依賴(lài)。試想,如果選擇直接構(gòu)建View和Connection的實(shí)例,則開(kāi)發(fā)者會(huì)面臨以下問(wèn)題(結(jié)合 F.I.R.S.T 原則考慮),主要來(lái)自于Connection:
使用真實(shí)的網(wǎng)絡(luò)連接必然大大增加單元測(cè)試的運(yùn)行時(shí)長(zhǎng),會(huì)違背 Fast 原則;
-
Twitter可能在任何時(shí)間點(diǎn)返回任何數(shù)據(jù),這樣會(huì)面臨兩種都很差的選擇:
- 1、在單個(gè)單元測(cè)試中處理各種響應(yīng)情況,這樣會(huì)使單元測(cè)試邏輯流程依賴(lài)于 Twitter 的具體響應(yīng)數(shù)據(jù),違背了 Isolated 原則;
- 2、針對(duì)不同的響應(yīng)數(shù)據(jù)編寫(xiě)不同的測(cè)試用例,但這樣不能保證所有用例的斷言都被執(zhí)行到,而且不同的響應(yīng)會(huì)執(zhí)行到不同的斷言,這樣違背了 Repeatly 原則;
Twitter一般不會(huì)返回錯(cuò)誤,如 404、500,而且也很難控制 Twitter 返回特定的錯(cuò)誤,同時(shí)也違背了 Self-verifying 原則;
F.I.R.S.T 原則(參考優(yōu)秀測(cè)試實(shí)踐原則):
Fast — 測(cè)試應(yīng)該能夠被經(jīng)常執(zhí)行;
Isolated — 測(cè)試本身不能依賴(lài)于外部因素或其他測(cè)試的結(jié)果;
Repeatable — 每次運(yùn)行測(cè)試都應(yīng)該產(chǎn)生相同的結(jié)果;
Self-verifying — 測(cè)試應(yīng)該依賴(lài)于斷言,不需要人為干預(yù);
Timely — 測(cè)試應(yīng)該和生產(chǎn)代碼一同書(shū)寫(xiě)
因此,在updateTweetView單元測(cè)試中直接構(gòu)建所依賴(lài)的View和Connection的實(shí)例是非常不明智的選擇。于是 mock 便應(yīng)運(yùn)而生。Mock 是用于在模塊的單元測(cè)試中,模擬 模塊所依賴(lài)的對(duì)象的特定行為或特定數(shù)據(jù)的 替身。例如:可以指定 mock 對(duì)象的方法返回固定的目標(biāo)數(shù)據(jù)(stubbing)、可以校驗(yàn) mock 對(duì)象的方法是否有被觸發(fā)(verifying)等等。Mock 可以使依賴(lài)的行為具備可確定、可編輯、可追蹤特性。
回到剛才的例子,由于不需要等待網(wǎng)絡(luò)數(shù)據(jù)同步返回,而是直接由 mock 返回模擬數(shù)據(jù),因此符合 Fast 原則;另外返回模擬數(shù)據(jù)高度可控,使之符合 Isolated、Repeatly、Self-verifying 原則。
既然有這么優(yōu)秀的選擇,那就可以正式著手編寫(xiě)測(cè)試用例了。接下來(lái)編寫(xiě)測(cè)試用例:Connection從 Twitter 拉取數(shù)據(jù)成功后,若Controller調(diào)用updateTweetView,View是否有刷新數(shù)據(jù)。首先需要明確單元測(cè)試用例的三個(gè)基本因素:
given:Connection的fetchTweet方法指定能返回 Twitter 數(shù)據(jù);
when:Controller實(shí)例調(diào)用了updateTweetView;
then:View是否有調(diào)用addTweet方法將 Twitter 數(shù)據(jù)顯示到界面;
2.3、編寫(xiě)測(cè)試用例
由于測(cè)試的目標(biāo)模塊是Controller因此需要構(gòu)建真實(shí)的實(shí)例,而依賴(lài)Connection和TweetView則只需構(gòu)建其 mock 替身,并為Controller所持有,此時(shí)Controller是不知道它們只是 mock 對(duì)象。由于 mock Connection 的 fetchTweets操作的時(shí)間、數(shù)據(jù)不可確定性,所以需要給 fetchTweets 打樁(stub)返回固定的 Twitter 數(shù)據(jù)。當(dāng)Controller實(shí)例調(diào)用updateTweetView方法時(shí),需要驗(yàn)證(verify)mock TweetView的addTweets:顯示 Twitter 數(shù)據(jù)到界面的操作被觸發(fā)。
@implementation ZJHTwitterViewControllerTests
- (void)testExample {
//--------- Given Start ---------//
// 1. 構(gòu)建Controller實(shí)例
ZJHTwitterViewController *controller = [ZJHTwitterViewController new];
// 2. Mock一個(gè)ZJHTwitterConnection實(shí)例
id mockConnection = OCMClassMock([ZJHTwitterConnection class]);
controller.connection = mockConnection;
// 創(chuàng)建一些數(shù)據(jù)
ZJHTweetModel *testTweet1 = [ZJHTweetModel new];
ZJHTweetModel *testTweet2 = [ZJHTweetModel new];
NSArray *tweetArray = @[testTweet1, testTweet2];
// 4. stub Connection 的 fetchTweets 方法使之固定返回Tweet模型數(shù)組
OCMStub([mockConnection fetchTweets]).andReturn(tweetArray);
// 4. Mock一個(gè)TweetView實(shí)例
id mockView = OCMClassMock([ZJHTweetView class]);
controller.tweetView = mockView;
//--------- When Start ---------//
// 5. 調(diào)用測(cè)試目標(biāo)方法updateTweetView。里面會(huì)調(diào)用fetchTweets,然后會(huì)得到我們存根的數(shù)組tweetArray
[controller updateTweetView];
//--------- Then Start ---------//
// 6. 驗(yàn)證 mock TweetView 的 addTweet: 顯示Tweet到界面的操作被觸發(fā)
OCMVerify([mockView addTweet:[OCMArg any]]);
}
@end
2.4、編寫(xiě)實(shí)現(xiàn)代碼
完成了updateTweetView方法的測(cè)試用例,就可以大致清楚updateTweetView需要處理什么數(shù)據(jù)(stub)、需要調(diào)用依賴(lài)的哪些方法(verify)。此時(shí)運(yùn)行該測(cè)試用例必然不通過(guò),因?yàn)檫€未實(shí)現(xiàn)updateTweetView。接下來(lái)開(kāi)始實(shí)現(xiàn)updateTweetView。具體代碼如下:
@implementation ZJHTwitterViewController
- (void)updateTweetView {
NSArray *tweets = [self.connection fetchTweets];
if (tweets != nil) { // 展示數(shù)據(jù)
for (ZJHTweetModel *item in tweets) {
[self.tweetView addTweet:item];
}
} else { // 處理異常情況
}
}
@end
此時(shí)運(yùn)行測(cè)試用例,用例通過(guò),因?yàn)闈M(mǎn)足了測(cè)試用例中的OCMVerify的條件:當(dāng)(given)connection固定正常返回 Tweet 數(shù)據(jù)時(shí),調(diào)用updateTweetView時(shí)(when),觸發(fā)了tweetView的addTweet:方法顯示 Tweet 數(shù)據(jù)到界面。
三、OCMock的示例使用
3.1、生成 Mock 對(duì)象三種方式的對(duì)比
3.1.1、需要測(cè)試的代碼
通過(guò)對(duì)Person類(lèi)的talk方法進(jìn)行測(cè)試舉例,其中也涉及Men類(lèi)以及Animaiton類(lèi),以下是三個(gè)類(lèi)的相關(guān)源碼。
MOPerson類(lèi):
@interface MOPerson()
@property(nonatomic,strong) MOMen *men;
@end
@implementation Person
- (void)talk:(NSString *)str {
[self.men logstr:str];
[MOAnimaiton logstr:str];
}
@end
MOMen類(lèi)
@implementation MOMen
-(NSString *)logstr:(NSString *)str {
NSLog(@"%@",str);
return str;
}
@end
MOAnimaiton類(lèi)
@implementation MOAnimaiton
+ (NSString *)logstr:(NSString *)str {
NSLog(@"%@",str);
return str;
}
@end
3.1.2、Nice Mock
NiceMock 創(chuàng)建的 mock 對(duì)象在進(jìn)行方法測(cè)試時(shí)會(huì)優(yōu)先調(diào)用實(shí)例方法,若未找到實(shí)例方法,會(huì)繼續(xù)調(diào)用同名的類(lèi)方法。因此該方法可以用來(lái)生成mock對(duì)象去測(cè)試類(lèi)方法也可以測(cè)試對(duì)象方法。使用場(chǎng)景:Nice mock 是比較友好的,當(dāng)一個(gè)沒(méi)有存根的方法被調(diào)用時(shí)他不會(huì)引起一個(gè)異常會(huì)驗(yàn)證通過(guò)。如果你不想自己對(duì)很多的方法進(jìn)行存根,那么使用 nice mock
- (void)testTalkNiceMock {
MOPerson *person1 = [MOPerson new]; // 新建person類(lèi)
id mockA = OCMClassMock([MOMen class]); // mock一個(gè)Men對(duì)象
person1.men = mockA;
[person1 talk:@"123"]; // person類(lèi)執(zhí)行方法
OCMVerify([mockA logstr:[OCMArg any]]); // 驗(yàn)證 logstr 方法有被調(diào)用
}
3.1.3、Strict Mock
使用方式:測(cè)試case如下,mockA是Strict Mock生成,要調(diào)用testTalkStrictMock方法,則該方法要使用stub進(jìn)行存根,否則最后的OCMVerifyAll(mockA)就會(huì)拋出異常。使用場(chǎng)景:這種方式創(chuàng)建的 mock 對(duì)象,如果調(diào)用未 stub(stub 代表存根)的方法,會(huì)拋出一個(gè)異常。這需要保證在 mock 的生命周期中每一個(gè)獨(dú)立調(diào)用的方法都是被存根的,這種方法使用比較嚴(yán)格,很少使用。
- (void)testTalkStrictMock {
id mockA = OCMStrictClassMock([MOPerson class]); // StrictMock生成mockA
OCMStub([mockA talk:[OCMArg any]]); // 使用stub進(jìn)行存根
[mockA talk:@"123"]; // 執(zhí)行talk方法
OCMVerifyAll(mockA); // 驗(yàn)證mock方法有沒(méi)有被執(zhí)行
}
3.1.4、Partial Mock
這樣創(chuàng)建的對(duì)象在調(diào)用方法時(shí):如果方法被 stub,調(diào)用 stub 后的方法,如果方法沒(méi)有被 stub,調(diào)用原來(lái)的對(duì)象的方法,該方法有限制只能 mock 實(shí)例對(duì)象。使用場(chǎng)景:當(dāng)調(diào)用一個(gè)沒(méi)有被存根的方法時(shí),會(huì)調(diào)用實(shí)際對(duì)象的該方法。當(dāng)不能很好的存根一個(gè)類(lèi)的方法時(shí),該技術(shù)是非常有用的。
- (void)testTalkPartialMock {
MOPerson *person1 = [MOPerson new];
MOMen *men = [MOMen new];
id mockA = OCMPartialMock(men);
// 如果方法被 stub,調(diào)用 stub 后的方法,如果方法沒(méi)有被 stub,調(diào)用原來(lái)的對(duì)象的方法
// OCMStub([mockA logstr:[OCMArg any]]).andReturn(@"456");;
person1.men = mockA;
[person1 talk:@"123"];
OCMVerify([mockA logstr:[OCMArg any]]);
}
3.2、預(yù)期的驗(yàn)證
3.2.1、需要測(cè)試的代碼
@implementation MOOCMockDemo
+ (void)handleLoadFinished:(NSDictionary *)info {
MOPerson *person = [MOPerson personWithInfo:info];
if ([person isValid]) {
[self handleLoadSuccessWithPerson:person];
[self showError:NO];
} else {
[self handleLoadFailWithPerson:person];
[self showError:YES];
}
}
+ (void)handleLoadSuccessWithPerson:(MOPerson *)person {
// do something
}
+ (void)handleLoadFailWithPerson:(MOPerson *)person {
// do something
}
+ (void)showError:(BOOL)error {
// do something
}
@end
3.2.2、驗(yàn)證預(yù)期
- (void)testMockExpect {
// 新建mock
id mock = OCMClassMock([MOOCMockDemo class]);
// 預(yù)期下列方法順序執(zhí)行
[mock setExpectationOrderMatters:YES];
// 預(yù)期 + 參數(shù)驗(yàn)證
OCMExpect([mock handleLoadSuccessWithPerson:[OCMArg checkWithBlock:^BOOL(id obj) {
MOPerson *person = (MOPerson *)obj;
return [person.name isEqualToString:@"momo"];
}]]);
// 預(yù)期方法
OCMExpect([mock showError:NO]);
// 預(yù)期不執(zhí)行
OCMReject([mock handleLoadFailWithPerson:[OCMArg any]]);
OCMReject([mock showError:YES]).ignoringNonObjectArgs; // 忽視參數(shù)
// 執(zhí)行方法
NSDictionary *info = @{@"name": @"momo"};
[MOOCMockDemo handleLoadFinished:info];
// 斷言
OCMVerifyAll(mock); // OCMVerifyAll 會(huì)驗(yàn)證前面的期望是否有效,只要有一個(gè)沒(méi)調(diào)用,就會(huì)出錯(cuò)。
OCMVerifyAllWithDelay(mock, 1); // 支持延遲驗(yàn)證
// 停止Mocking
[mock stopMocking];
}
3.3、網(wǎng)絡(luò)接口的模擬
3.3.1、需要測(cè)試的代碼
@implementation ZJHOrderListViewController
- (void)getListData { // 接口請(qǐng)求獲取網(wǎng)絡(luò)數(shù)據(jù)
[ZJHNetworkTool requestUrl:@"url" param:@{} completion:^(NSDictionary * _Nonnull respondDic) {
self.dataArr = respondDic[@"data"];
[self refreshView];
}];
}
- (void)refreshView { // 刷新頁(yè)面
NSLog(@"***ZJH refreshView : %@", self.dataArr);
}
@end
3.3.2、網(wǎng)絡(luò)接口模擬
- (void)testMockNetwork {
ZJHOrderListViewController *listVc = [ZJHOrderListViewController new];
id mockManager = OCMClassMock([ZJHNetworkTool class]);
// mock請(qǐng)求方法,并返回特定參數(shù)
OCMStub([mockManager requestUrl:[OCMArg any] param:[OCMArg any] completion:[OCMArg any]]).andDo(^(NSInvocation *invocation){
void (^successBlock)(NSDictionary *respondDic) = nil;
[invocation getArgument:&successBlock atIndex:4];
successBlock( @{ @"data" : @[@"a", @"b", @"c"] } );
});
[listVc getListData];
OCMVerifyAll(mockManager);
}
以上就是在調(diào)用 getListData 方法內(nèi)部調(diào)用了接口,該方法就可以在調(diào)用接口后模擬需要的返回?cái)?shù)據(jù),successBlock 中的就是返回的測(cè)試數(shù)據(jù)。本方式是通過(guò)獲取接口調(diào)用的方法簽名,獲取 successBlock 成功回調(diào)傳參并手動(dòng)調(diào)用。同樣可以模擬接口失敗的情況,只需獲取到簽名中的對(duì)應(yīng)的失敗回調(diào)就可以實(shí)現(xiàn)了。使用場(chǎng)景:書(shū)寫(xiě)單元測(cè)試方法時(shí)涉及網(wǎng)絡(luò)接口的模擬,通過(guò)該方式 mock 接口返回結(jié)果。
四、OCMock基本API詳解
本章根據(jù)官方文檔 Documentation 改編而來(lái),可以在里查看如何詳細(xì)使用OCMock。
4.1、創(chuàng)建模擬對(duì)象 Creating mock objects
4.1.1、模擬實(shí)例 Class mocks
// 根據(jù)類(lèi),模擬其實(shí)例
id mockPerson = OCMClassMock([MOPerson class]);
4.1.2、模擬代理 Protocol mocks
// 根據(jù)協(xié)議名,模擬已經(jīng)實(shí)現(xiàn)協(xié)議的實(shí)例
id mockProtocol = OCMProtocolMock(@protocol(MOTitleLineViewDelegate));
// 然后mock協(xié)議方法
4.1.3、嚴(yán)格模擬類(lèi)和協(xié)議 Strict class and protocol mocks
// 在收到?jīng)]有預(yù)期(expect)的方法時(shí)引發(fā)異常
id strictMockClass = OCMStrictClassMock([MOPerson class]);
id strictMockProtocol = OCMStrictProtocolMock(@protocol(MOTitleLineViewDelegate));
4.1.4、部分模擬 Partial mocks
這里介紹一個(gè)定義:Stub,存根,就是模擬一個(gè)函數(shù)。
MOPerson *aPerson = [[MOPerson alloc] init];
id partialMockPerson = OCMPartialMock(aPerson);
調(diào)用一個(gè)函數(shù):已經(jīng)存根的就觸發(fā)存根的(Stub);未存根的就觸發(fā)原有實(shí)例的(aPerson)。
4.1.5、觀察者模擬 Observer mocks
用官方的XCTNSNotificationExpectation
4.2、存根方法 Stubbing methods
4.2.1、模擬方法的返回值 Stubbing methods that return objects
OCMStub([partialMockPerson name]).andReturn(@"moxiaoyan");
OCMStub([mock aMethodReturningABoolean]).andReturn(YES);
4.2.2、委托給另一個(gè)方法 Stubbing methods that return values
MOPerson *anotherPerson = [[MOPerson alloc] init];
// 另一個(gè)對(duì)象的方法,方法簽名需要一致
OCMStub([partialMockPerson name]).andCall(anotherPerson, @selector(name));
4.2.3、委托給一個(gè)block Delegating to another method
OCMStub([partialMockPerson name]).andDo(^(NSInvocation *invocation){
// 調(diào)用name方法時(shí),將會(huì)調(diào)用這個(gè)block
// invocation會(huì)攜帶方法參數(shù)
// invocation可以設(shè)置返回值
});
OCMStub([partialMock name]).andDo(nil);
4.2.4、委托給塊 Delegating to a block
模擬對(duì)象將在調(diào)用函數(shù)時(shí),調(diào)用該Block。該Block可以從調(diào)用的對(duì)象中讀取參數(shù),并可以設(shè)置返回值。
OCMStub([mock someMethod]).andDo(^(NSInvocation *invocation) {
/* block that handles the method invocation */
});
4.2.5、模擬 通過(guò)參數(shù)返回值的方法 的返回值 Returning values in pass-by-reference arguments
4.2.5.1、對(duì)象參數(shù)
通過(guò)參數(shù)傳回值:
// 模擬 應(yīng)該返回的參數(shù)值
NSError *error = [NSError errorWithDomain:@"獲取friends失敗(stubbed)" code:001 userInfo:nil];
OCMStub([partialMockPerson loadFriendsWithError:[OCMArg setTo:error]]);
// 函數(shù)調(diào)用,獲得模擬的值
NSError *resultError = nil;
[partialMockPerson loadFriendsWithError:&resultError];
NSLog(@"%@", resultError); // 001, 獲取friends失敗(stubbed)
4.2.5.2、非對(duì)象參數(shù)
OCMStub([mock someMethodWithReferenceArgument:[OCMArg setToValue:OCMOCK_VALUE((int){aValue})]]);
4.2.6、模擬block參數(shù) Invoking block arguments
// invokeBlock默認(rèn)模擬,參數(shù)都為默認(rèn)值
OCMStub([partialMockPerson deviceWithComplete:[OCMArg invokeBlock]]);
[partialMockPerson deviceWithComplete:^(NSString * _Nonnull value) {
NSLog(@"%@", value); // nil
}];
// invokeBlockWithArgs模擬,可以設(shè)置參數(shù)值
OCMStub([partialMockPerson deviceWithComplete:[OCMArg invokeBlockWithArgs:@"iPhone"]]);
[partialMockPerson deviceWithComplete:^(NSString * _Nonnull value) {
NSLog(@"%@", value); // iPhone
}];
4.2.7、拋出異常 Throwing exceptions
設(shè)置函數(shù)被調(diào)用時(shí),拋出異常:
NSException *exception = [[NSException alloc] initWithName:@"獲取name異常" reason:@"name為空" userInfo:nil];
OCMStub([partialMockPerson name]).andThrow(exception);
4.2.8、發(fā)出通知 Posting notifications
設(shè)置函數(shù)被調(diào)用是,發(fā)出通知(notify)
NSNotification *notify = [NSNotification notificationWithName:@"通知" object:self userInfo:nil];
OCMStub([partialMockPerson name]).andPost(notify);
4.2.9、鏈接模擬方法 Chaining stub actions
諸如andReturn和 之類(lèi)的所有操作andPost都可以鏈接
// 模擬對(duì)象將發(fā)布通知并返回值
OCMStub([mock someMethod]).andPost(aNotification).andReturn(aValue);
4.2.10、轉(zhuǎn)發(fā)給真正的對(duì)象/類(lèi) Forwarding to the real object / class
當(dāng)使用部分模擬實(shí)例和模擬類(lèi)方法時(shí),可以將存根方法轉(zhuǎn)發(fā)給真實(shí)對(duì)象或類(lèi)。這僅在鏈接操作或使用期望時(shí)有用。
OCMStub([partialMockPerson name]).andForwardToRealObject();
4.2.11、什么也不做 Doing nothing
可以將nil而不是塊傳遞給andDo。這僅在部分模擬或模擬類(lèi)方法時(shí)有用。在這些情況下,使用andDo(nil)有效地抑制了現(xiàn)有類(lèi)中的行為。
OCMStub([mock someMethod]).andDo(nil);
4.2.12、滿(mǎn)足XCTest的期望(需要OCMock3.8)Fulfilling XCTest expectations
當(dāng)調(diào)用該方法時(shí),XCTest 框架中的期望得到滿(mǎn)足:
XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"XCTest的期望"];
OCMStub([partialMockPerson name]).andFulfill(expectation);
4.2.13、記錄消息(需要OCMock3.8)Logging messages
OCMStub([partialMockPerson name]).andLog(@"%@", @"hehe");
調(diào)用該方法時(shí),format通過(guò)NSLog。很可能您想在一個(gè)鏈中使用它,可能后跟andReturn()或andForwardToRealObject()
4.2.14、打開(kāi)調(diào)試,斷點(diǎn)會(huì)生效(需要OCMock3.8)
OCMStub([partialMockPerson name]).andBreak();
當(dāng)調(diào)用該方法時(shí),調(diào)試器被打開(kāi),就好像一個(gè)斷點(diǎn)被命中一樣。堆棧將在 OCMock 的實(shí)現(xiàn)中的某個(gè)地方結(jié)束,但是如果您進(jìn)一步查看,越過(guò)__forwarding__幀,您應(yīng)該能夠看到您的代碼調(diào)用該方法的位置。
4.3、驗(yàn)證交互 Verifying interactions
4.3.1、驗(yàn)證方法已調(diào)用 Verify-after-running
[aPerson name];
OCMVerify([partialMockPerson name]);
驗(yàn)證name已被測(cè)試代碼調(diào)用。如果尚未調(diào)用該方法,則會(huì)報(bào)告錯(cuò)誤。
4.3.2、驗(yàn)證Stubbed的方法被調(diào)用 Stubs and verification
OCMStub([partialMockPerson name]).andReturn(@"momo");
[aPerson name];
OCMVerify([partialMockPerson name]);
可以存根一個(gè)方法并仍然驗(yàn)證它是否已被調(diào)用。
4.3.3、量詞要求 Quantifiers requires
驗(yàn)證方法被調(diào)用的次數(shù):
OCMVerify(atLeast(2), [partialMockPerson name]);
OCMVerify(never(), [partialMock doStuff]);
OCMVerify(times(0), [partialMock doStuff]);
OCMVerify(times(n), [partialMock doStuff]);
OCMVerify(atLeast(n), [partialMock doStuff]);
OCMVerify(atMost(n), [partialMock doStuff]);
4.4、參數(shù)約束 Argument constraints
4.4.1、任何約束 The any constraint
// stub方法,可以響應(yīng)任何調(diào)用
OCMStub([partialMockPerson addChilden:[OCMArg any]]); // 參數(shù)是任何對(duì)象
OCMStub([partialMockPerson takeMoney:[OCMArg anyPointer]]); // 參數(shù)是任何指針
OCMStub([partialMockPerson changeWithSelector:[OCMArg anySelector]]); // 參數(shù)是任何選擇子
4.4.2、忽視沒(méi)有對(duì)象參數(shù) Ignoring non-object arguments
stub方法,可以響應(yīng)非對(duì)象參數(shù)的調(diào)用(可以響應(yīng)參數(shù)沒(méi)有通過(guò)的調(diào)用:無(wú)論是對(duì)象參數(shù) or 非對(duì)象參數(shù))
OCMStub([partialMockPerson setAge:0]).ignoringNonObjectArgs();
4.4.3、匹配參數(shù) Matching arguments
stub方法,僅響應(yīng)匹配的參數(shù)的調(diào)用
MOPerson *bPerson = [[MOPerson alloc] init];
OCMStub([partialMockPerson addChilden:bPerson]);
OCMStub([partialMockPerson addChilden:[OCMArg isNil]]);
OCMStub([partialMockPerson addChilden:[OCMArg isNotNil]]);
OCMStub([partialMockPerson addChilden:[OCMArg isNotEqual:bPerson]]);
OCMStub([partialMockPerson addChilden:[OCMArg isKindOfClass:[MOPerson class]]]);
會(huì)觸發(fā) anObject 的 aSelector 方法,并將參數(shù)傳入
在該方法中判斷參數(shù)是否通過(guò),通過(guò)就:返回YES, 否則:返回NO
id anObject = nil;
SEL aSelector = @selector(addChilden:);
OCMStub([partialMockPerson addChilden:[OCMArg checkWithSelector:aSelector onObject:anObject]]);
OCMStub([partialMockPerson addChilden:[OCMArg checkWithBlock:^BOOL(id value) {
// 判斷參數(shù)是否通過(guò),通過(guò)就:返回YES, 否則:返回NO
return YES;
}]]);
4.4.4、使用Hamcrest匹配
OCMStub([partialMockPerson addChilden:startsWith(@"foo")]);
4.5、模擬類(lèi)方法 Mocking class methods
4.5.1、存根類(lèi)方法 Stubbing class methods
id mockPerson = OCMClassMock([MOPerson class]);
OCMStub([mockPerson mo_className]).andReturn(@"XXMOPerson");
4.5.2、消除類(lèi)和實(shí)例方法的歧義 Disambiguating class and instance methods
// (1)此時(shí)如果沒(méi)有同名的實(shí)例方法,mo_className類(lèi)方法是可以被正確Stub的
NSString *className1 = [MOPerson mo_className]; // XXMOPerson
// (2)但是如果實(shí)例方法有跟之同名時(shí):
NSString *instanceName = [mockPerson mo_className]; // XXMOPerson
NSString *className2 = [MOPerson mo_className]; // class MOPerson
// 則需要用一下方法進(jìn)行Stub
OCMStub(ClassMethod([mockPerson mo_className])).andReturn(@"MOMOPerson");
NSString *className3 = [MOPerson mo_className]; // XXMOPerson
4.5.3、驗(yàn)證類(lèi)方法已執(zhí)行 Verifying invocations of class methods
[mockPerson mo_className];
OCMVerify([mockPerson mo_className]);
4.5.4、恢復(fù)類(lèi) Disambiguating class and instance methods
[mockPerson stopMocking];
4.6、部分模擬 Partial mocks
4.6.1、存根方法 Stubbing methods
id partialMockPerson = OCMPartialMock(aPerson);
OCMStub([partialMockPerson mo_className]).andReturn(@"Partail Class");
NSString *partialName = [partialMockPerson mo_className]; // Partail Class
NSString *personName = [aPerson mo_className]; // Partail Class
4.6.2、驗(yàn)證調(diào)用 Verifying invocations
[partialMockPerson mo_className];
OCMVerify([partialMockPerson mo_className]);
4.6.3、恢復(fù)對(duì)象 Restoring the object
[partialMockPerson stopMocking];
4.7、嚴(yán)格的模擬和期望 Strict mocks and expectations
4.7.1、設(shè)置期望-運(yùn)行-驗(yàn)證 Expect-run-verify
id mockPerson = OCMClassMock([MOPerson class]);
OCMExpect([mockPerson addChilden:[OCMArg isNotNil]]);
[mockPerson addChilden:[MOPerson new]]; // 只要有一次不為nil,就通過(guò)了驗(yàn)證!
[mockPerson addChilden:nil];
OCMVerifyAll(mockPerson);
4.7.2、嚴(yán)格的模擬和快速失敗 Strict mocks and failing fast
id strictPerson = OCMStrictClassMock([MOPerson class]);
[strictPerson mo_className]; // 沒(méi)有期望該方法的調(diào)用,所以會(huì)測(cè)試失敗
4.7.3、存根和期望 Stub actions and expect
也可以在期望的情況下使用andReturn、andThrow等。這將在調(diào)用方法時(shí)運(yùn)行存根操作,并在驗(yàn)證時(shí)確保該方法被實(shí)際調(diào)用
OCMExpect([strictPerson mo_className]).andReturn(@"instance_MOPerson");
OCMExpect([strictPerson mo_className]).andThrow([NSException ...]);
[strictPerson mo_className];
OCMVerifyAll(strictPerson);
4.7.4、延遲驗(yàn)證 Verify with delay
OCMExpect([strictPerson mo_className]);
[strictPerson mo_className];
OCMVerifyAllWithDelay(strictPerson, 4.0); // NSTimeInterval, 通常會(huì)在滿(mǎn)足預(yù)期后立即返回
4.7.5、按順序驗(yàn)證 Verifying in order
一旦調(diào)用了不在“預(yù)期列表”中的下一個(gè)方法,模擬就會(huì)快速失敗并拋出異常。
[strictPerson setExpectationOrderMatters:YES];
OCMExpect([strictPerson mo_className]);
OCMExpect([strictPerson addChilden:[OCMArg any]]);
// 調(diào)用順序錯(cuò)了,測(cè)試就會(huì)失敗
[strictPerson mo_className];
[strictPerson addChilden:nil];
4.8、觀察者模擬 Observer mocks
從OCMock 4.8開(kāi)始不推薦使用觀察者模擬。請(qǐng)改用XCTNSNotificationExpectation
4.9、進(jìn)階主題 Advanced topics
4.9.1、快速失敗的常規(guī)模擬 (需要OCMock3.3) Failing fast for regular (nice) mocks
strict模擬:調(diào)用未存根的方法會(huì)拋出異常
常規(guī)模擬:只是返回默認(rèn)值;可以為函數(shù)配置快速失敗:
id mockPerson = OCMClassMock([MOPerson class]);
OCMReject([mockPerson mo_className]);
在這種情況下,模擬將接受所有方法,除了mo_className,如果調(diào)用該函數(shù),則將引發(fā)異常。
4.9.2、重新驗(yàn)證失敗后快速拋出異常 Re-throwing fail fast exceptions in verify all
在快速失敗模式下,異??赡懿粫?huì)導(dǎo)致測(cè)試失?。ㄈ纾寒?dāng)方法的調(diào)用堆棧未在測(cè)試中結(jié)束時(shí))
OCMerifyAll調(diào)用時(shí),快速失敗異常將重新引發(fā),可以確保檢測(cè)到來(lái)自通知等不需要的調(diào)用
4.9.3、存根創(chuàng)建對(duì)象的方法 Stubbing methods that create objects
MOPerson *myPerson = [[MOPerson alloc] init];
OCMStub([mockPerson copy]).andReturn(myPerson);
會(huì)根據(jù)方法名,自動(dòng)返回對(duì)象的:alloc、new、copy、mutableCopy (引用計(jì)數(shù))
注意:init方法無(wú)法Stub,因?yàn)樵摲椒ㄊ怯赡M本身實(shí)現(xiàn)的。 當(dāng)init方法再次被調(diào)用時(shí),會(huì)直接返回
模擬對(duì)象self
這樣就可以有效的對(duì)alloc、init進(jìn)行Stub
4.9.4、基于實(shí)現(xiàn)的方法交換 Instance-based method swizzling
MOPerson *person = [[MOPerson alloc] init];
id partialMockPerson = OCMPartialMock(person);
OCMStub([partialMockPerson mo_className]).andCall(myPerson, @selector(name));
方法的名稱(chēng)可以不同,但是簽名應(yīng)該相同
4.9.5、打破保留周期 Breaking retain cycles
[mockPerson stopMocking];
[partialMockPerson stopMocking];
4.9.6、禁用短語(yǔ)法 Disabling short syntax
禁用 沒(méi)有前綴的宏:ClassMethod()、atLeast()、…
用有前綴的宏:OCMClassMethod()、OCMAtLeast()、…
4.9.7、停止為特定類(lèi)創(chuàng)建模擬 (需要OCMock3.8) Stopping creation of mocks for specific classes
一些框架在運(yùn)行時(shí)動(dòng)態(tài)更改對(duì)象的類(lèi)。OCMock這樣做是為了實(shí)現(xiàn)部分模擬,并且Foundation框架將更改類(lèi)作為(KVO)機(jī)制的一部分。
如果不仔細(xì)協(xié)調(diào),可能會(huì)導(dǎo)致意外行為或crash。
OCMock知道KVO,并小心避免與之發(fā)生沖突
對(duì)于其它框架,OCMock僅提供了一種選擇退出模擬以免發(fā)生意外行為的機(jī)制
+ (BOOL)supportsMocking:(NSString **)reason {
*reason = @"Don't want to be mocked."
return NO;
}
通過(guò)實(shí)現(xiàn)上面的方法,一個(gè)類(lèi)可以選擇不被Mock。當(dāng)開(kāi)發(fā)人員嘗試為此類(lèi)創(chuàng)建模擬程序時(shí),將引發(fā)異常,解釋問(wèn)題說(shuō)在該方法在單獨(dú)調(diào)用中返回不同的值是可以接受的,這使它在運(yùn)行時(shí)對(duì)特定條件做出反應(yīng)。如果該方法為reason賦值,返回值將被忽略。對(duì)于所有未實(shí)現(xiàn)此方法的類(lèi),OCMock假定可以接受Mock
4.9.8、檢查部分Mock (需要OCMock3.8) Checking for partial mock
判斷是否 是部分模擬對(duì)象
BOOL isPartialMockObj = OCMIsSubclassOfMockClass(objc_getClass(partialMockPerson));
4.10、局限性 Limitations
4.10.1、一次只能有一個(gè)Mock可以在給定類(lèi)上存根方法
不要這樣做:
id mock1 = OCMClassMock([SomeClass class]);
OCMStub([mock1 aClassMethod]);
id mock2 = OCMClassMock([SomeClass class]);
OCMStub([mock2 anotherClassMethod]);
如果添加了存根類(lèi)方法的模擬對(duì)象未釋放,則存根方法將持續(xù)存在,即使在測(cè)試中也是如此。如果多個(gè)模擬對(duì)象同時(shí)操作同一類(lèi),則行為將不可預(yù)測(cè)。
4.10.2、期望Stub方法無(wú)效
id mock = OCMStrictClassMock([SomeClass class]);
OCMStub([mock someMethod]).andReturn(@"a string");
OCMExpect([mock someMethod]);
由于當(dāng)前實(shí)現(xiàn)了模擬對(duì)象的方法,Stub會(huì)處理所有對(duì)它的調(diào)用。意味著即使調(diào)用了該方法,驗(yàn)證也會(huì)失敗。避免此問(wèn)題:
- 方法1:通過(guò)
andReturn在Expect語(yǔ)句中添加 - 方法2:在設(shè)置期望之后存根
4.10.3、不能為某些特殊類(lèi)創(chuàng)建部分模擬
id partialMockForString = OCMPartialMock(@"Foo"); // 會(huì)拋出異常
NSDate *date = [NSDate dateWithTimeIntervalSince1970:0];
id partialMockForDate = OCMPartialMock(date); // 會(huì)對(duì)一些架構(gòu)造成影響嗎
無(wú)法為 toll-free bridged 類(lèi)的實(shí)例創(chuàng)建局部模擬
無(wú)法為 某些實(shí)例創(chuàng)建以標(biāo)記指針表示的對(duì)象,如:NSString、在某些體系結(jié)構(gòu)上、NSDate在某些體系結(jié)構(gòu)上
4.10.4、某些方法無(wú)法存根或驗(yàn)證
id partialMockForString = OCMPartialMock(anObject);
OCMStub([partialMock class]).andReturn(someOtherClass); // will not work
無(wú)法模擬許多核心運(yùn)行時(shí)方法。包括:init、class、methodSignatureForSelector:、forwardInvocation:、respondsToSelector等等
4.10.5、NSString和NSArray上的類(lèi)方法無(wú)法存根或驗(yàn)證
// 無(wú)法生效、該方法將不會(huì)被存根
id stringMock = OCMClassMock([NSString class]);
// 無(wú)法在NSString和NSArray上存根或驗(yàn)證類(lèi)方法。嘗試這樣做沒(méi)有任何效果。
OCMStub([stringMock stringWithContentsOfFile:[OCMArg any] encoding:NSUTF8StringEncoding error:[OCMArg setTo:nil]]);
4.10.6、NSManagedObject的類(lèi)方法及其子類(lèi)無(wú)法存根或驗(yàn)證
// 無(wú)法生效、該方法將不會(huì)被存根
id mock = OCMClassMock([MyManagedObject class]);
// 無(wú)法在其N(xiāo)SManagedObject或其子類(lèi)上存根或驗(yàn)證類(lèi)方法。嘗試這樣做沒(méi)有任何效果。
OCMStub([mock someClassMethod]).andReturn(nil);
4.10.7、無(wú)法驗(yàn)證 NSObject 上的方法
id mock = OCMClassMock([NSObject class]);
/* run code under test, which calls awakeAfterUsingCoder: */
OCMVerify([mock awakeAfterUsingCoder:[OCMArg any]]); // still fails
不可能使用在 NSObject 中實(shí)現(xiàn)的方法或其上的類(lèi)別進(jìn)行運(yùn)行后驗(yàn)證。
在某些情況下,可以對(duì)方法進(jìn)行存根,然后對(duì)其進(jìn)行驗(yàn)證。
當(dāng)方法在子類(lèi)中被覆蓋時(shí),可以使用運(yùn)行后驗(yàn)證
4.10.8、無(wú)法驗(yàn)證核心 Apple 類(lèi)中的私有方法
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
不可能在核心 Apple 類(lèi)中使用私有方法運(yùn)行后驗(yàn)證。
具體來(lái)說(shuō),在以 NS 或 UI 作為前綴的類(lèi)中,所有帶有下劃線(xiàn)前綴和/或后綴的方法。
在某些情況下,可以對(duì)方法進(jìn)行存根,然后對(duì)其進(jìn)行驗(yàn)證
4.10.9、運(yùn)行后驗(yàn)證不能使用延遲
目前無(wú)法驗(yàn)證具有延遲的方法。這目前只能使用下面在嚴(yán)格模擬和期望中描述的expect-run-verify方法。
4.10.10、測(cè)試中使用多線(xiàn)程
OCMock 不是完全線(xiàn)程安全的。直到 4.2.x 版本 OCMock 根本不知道線(xiàn)程。來(lái)自多個(gè)線(xiàn)程的模擬對(duì)象上的任何操作組合都可能導(dǎo)致問(wèn)題并使測(cè)試失敗。
從 OCMock 4.3 開(kāi)始,仍然需要從單個(gè)線(xiàn)程調(diào)用所有設(shè)置和驗(yàn)證操作,最好是測(cè)試運(yùn)行程序的主線(xiàn)程。
但是,可以從多個(gè)線(xiàn)程使用模擬對(duì)象。模擬對(duì)象甚至可以在不同的線(xiàn)程中使用,而其設(shè)置在主線(xiàn)程中繼續(xù)進(jìn)行。
五、部分補(bǔ)充
5.1、單例的mock
不能直接mock單例的,會(huì)引起mock沖突。推薦的寫(xiě)法:
// 每次mock alloc 一個(gè)單例
id center = OCMPartialMock([[QLLoginCenter alloc] init]);
// mock 它的 sharedInstance 方法
OCMStub([[center classMethod] sharedInstance]).andReturn(center);
參考鏈接:
iOS中的測(cè)試:OCMock:http://www.itdecent.cn/p/44ea034ac755
iOS_單元測(cè)試三之OCMock使用:https://blog.csdn.net/Margaret_MO/article/details/115420007
iOS_單元測(cè)試三之OCMockDemo:https://blog.csdn.net/Margaret_MO/article/details/118341525
iOS 單元測(cè)試之常用框架 OCMock 詳解:https://www.51cto.com/article/707544.html
iOS單元測(cè)試-06-OCMoke和Stub詳解:http://www.itdecent.cn/p/6fd98f95d1ba