iOS單元測(cè)試之OCMock的簡(jiǎn)介和使用

一、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。

mock依賴(lài)關(guān)系.png

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)ZJHTwitterViewControllerupdateTweetView方法,該方法通過(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ì)ViewConnection的依賴(lài)。試想,如果選擇直接構(gòu)建ViewConnection的實(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)的ViewConnection的實(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è)基本因素:

givenConnectionfetchTweet方法指定能返回 Twitter 數(shù)據(jù);

whenController實(shí)例調(diào)用了updateTweetView;

thenView是否有調(diào)用addTweet方法將 Twitter 數(shù)據(jù)顯示到界面;

2.3、編寫(xiě)測(cè)試用例

由于測(cè)試的目標(biāo)模塊是Controller因此需要構(gòu)建真實(shí)的實(shí)例,而依賴(lài)ConnectionTweetView則只需構(gòu)建其 mock 替身,并為Controller所持有,此時(shí)Controller是不知道它們只是 mock 對(duì)象。由于 mock ConnectionfetchTweets操作的時(shí)間、數(shù)據(jù)不可確定性,所以需要給 fetchTweets 打樁(stub)返回固定的 Twitter 數(shù)據(jù)。當(dāng)Controller實(shí)例調(diào)用updateTweetView方法時(shí),需要驗(yàn)證(verify)mock TweetViewaddTweets:顯示 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ā)了tweetViewaddTweet:方法顯示 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ā) anObjectaSelector 方法,并將參數(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

也可以在期望的情況下使用andReturnandThrow等。這將在調(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、newcopy、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ò)andReturnExpect語(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ō),在以 NSUI 作為前綴的類(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容