iOS單元測試之OCMock的簡介和使用

一、OCMock簡介

1.1、Mock介紹

作為一個動詞,mock是模擬、模仿的意思;作為一個名詞,mock是能夠模仿真實對象行為的模擬對象。在軟件測試中,mock所模擬的對象是什么呢?它一定不是我們所測試的對象,而是 SUT(Software Under Test:測試的對象) 的依賴(dependency)。換句話說,mock 的作用是模擬 SUT 依賴對象的行為。

文字不好理解,我們畫個圖,如下圖所示,被測試對象是 A,A 依賴的是B,B 依賴的是 C。而我們要 mock 的是 B 的行為。圖中 A 就是 SUT。

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

1.2、OCMock介紹

OCMock是一個用于為iOS或Mac OS X項目配置Mock測試的開源項目。

其實現(xiàn)思想就是根據(jù)要mock的對象的class來創(chuàng)建一個對應(yīng)的對象,并且設(shè)置好該對象的屬性和調(diào)用預定方法后的動作(例如返回一個值,調(diào)用代碼塊,發(fā)送消息等等),然后將其記錄到一個數(shù)組中,接下來開發(fā)者主動調(diào)用該方法,最后做一個verify(驗證),從而判斷該方法是否被調(diào)用,或者調(diào)用過程中是否拋出異常等。

其實就是可以把它當做我們偽造的一個對象,我們給它一些預設(shè)的值之類的,然后就可以進行對應(yīng)的驗證了。

1.3、OCMock集成

項目集成 OCMock 第三方庫,這個使用 pod 工具直接安裝OCMock框架即可。若使用 iBiu 工具安裝 OCMock 庫需在 podfile 文件同級創(chuàng)建 Podfile.custom。

使用普通的 pod 文件相同格式添加 OCmock 如下:

source 'https://github.com/CocoaPods/Specs.git'
pod 'OCMock'

二、OCMock的入門

關(guān)于為什么需要 mock,OCMock 官網(wǎng)的 Introduction 舉了以下一個例子(是個標準的 TDD 開發(fā)流程,值得學習一下):開發(fā)者需要開發(fā)一個從 Twitter 上拉取數(shù)據(jù),然后更新用戶界面的模塊,如何應(yīng)用 TDD 編寫該模塊的單元測試。接下來的內(nèi)容,是根據(jù) TDD 流程劃分小節(jié),關(guān)于 mock 的存在價值則分散在每個小節(jié)各處。

點擊下載Demo:ZJHUnitTestDemo

2.1、示例模塊劃分

首先,劃分大致模塊,例如最簡單的 MVC 模塊劃分方式,以確定接口。

Controller

@interface ZJHTwitterViewController : UIViewController
@property (nonatomic, strong) ZJHTwitterConnection *connection;
@property (nonatomic, strong) ZJHTweetView *tweetView;
- (void)updateTweetView;
@end

Data Source

@interface ZJHTwitterConnection : NSObject
// 檢索新推文的方法。它返回一個ZJHTweetModel對象數(shù)組,如果無法處理請求,則返回nil。
- (NSArray <ZJHTweetModel *> *)fetchTweets;
@end

View

@interface ZJHTweetView : UIView
// 一個將單個推文添加到視圖的方法
- (void)addTweet:(ZJHTweetModel *)aTweet;
@en

2.2、確定測試用例三要素

選定實現(xiàn)ZJHTwitterViewControllerupdateTweetView方法,該方法通過調(diào)用connection成員的fetchTweets獲取 Twitter 數(shù)據(jù),然后調(diào)用tweetView成員的addTweet:將數(shù)據(jù)顯示到界面。TDD(Test Driven Development:測試驅(qū)動開發(fā)) 是測試先行,因此先編寫針對updateTweetView方法的單元測試。在此之前,需要考慮如何處理ControllerViewConnection的依賴。試想,如果選擇直接構(gòu)建ViewConnection的實例,則開發(fā)者會面臨以下問題(結(jié)合 F.I.R.S.T 原則考慮),主要來自于Connection

  • 使用真實的網(wǎng)絡(luò)連接必然大大增加單元測試的運行時長,會違背 Fast 原則;

  • Twitter可能在任何時間點返回任何數(shù)據(jù),這樣會面臨兩種都很差的選擇:

    • 1、在單個單元測試中處理各種響應(yīng)情況,這樣會使單元測試邏輯流程依賴于 Twitter 的具體響應(yīng)數(shù)據(jù),違背了 Isolated 原則;
    • 2、針對不同的響應(yīng)數(shù)據(jù)編寫不同的測試用例,但這樣不能保證所有用例的斷言都被執(zhí)行到,而且不同的響應(yīng)會執(zhí)行到不同的斷言,這樣違背了 Repeatly 原則;
  • Twitter一般不會返回錯誤,如 404、500,而且也很難控制 Twitter 返回特定的錯誤,同時也違背了 Self-verifying 原則;

F.I.R.S.T 原則(參考優(yōu)秀測試實踐原則):

Fast — 測試應(yīng)該能夠被經(jīng)常執(zhí)行;
Isolated — 測試本身不能依賴于外部因素或其他測試的結(jié)果;
Repeatable — 每次運行測試都應(yīng)該產(chǎn)生相同的結(jié)果;
Self-verifying — 測試應(yīng)該依賴于斷言,不需要人為干預;
Timely — 測試應(yīng)該和生產(chǎn)代碼一同書寫

因此,在updateTweetView單元測試中直接構(gòu)建所依賴的ViewConnection的實例是非常不明智的選擇。于是 mock 便應(yīng)運而生。Mock 是用于在模塊的單元測試中,模擬 模塊所依賴的對象的特定行為或特定數(shù)據(jù)的 替身。例如:可以指定 mock 對象的方法返回固定的目標數(shù)據(jù)(stubbing)、可以校驗 mock 對象的方法是否有被觸發(fā)(verifying)等等。Mock 可以使依賴的行為具備可確定、可編輯、可追蹤特性。

回到剛才的例子,由于不需要等待網(wǎng)絡(luò)數(shù)據(jù)同步返回,而是直接由 mock 返回模擬數(shù)據(jù),因此符合 Fast 原則;另外返回模擬數(shù)據(jù)高度可控,使之符合 Isolated、Repeatly、Self-verifying 原則。

既然有這么優(yōu)秀的選擇,那就可以正式著手編寫測試用例了。接下來編寫測試用例:Connection從 Twitter 拉取數(shù)據(jù)成功后,若Controller調(diào)用updateTweetView,View是否有刷新數(shù)據(jù)。首先需要明確單元測試用例的三個基本因素:

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

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

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

2.3、編寫測試用例

由于測試的目標模塊是Controller因此需要構(gòu)建真實的實例,而依賴ConnectionTweetView則只需構(gòu)建其 mock 替身,并為Controller所持有,此時Controller是不知道它們只是 mock 對象。由于 mock ConnectionfetchTweets操作的時間、數(shù)據(jù)不可確定性,所以需要給 fetchTweets 打樁(stub)返回固定的 Twitter 數(shù)據(jù)。當Controller實例調(diào)用updateTweetView方法時,需要驗證(verify)mock TweetViewaddTweets:顯示 Twitter 數(shù)據(jù)到界面的操作被觸發(fā)。

@implementation ZJHTwitterViewControllerTests

- (void)testExample {
    //--------- Given Start ---------//
    // 1. 構(gòu)建Controller實例
    ZJHTwitterViewController *controller = [ZJHTwitterViewController new];
    
    // 2. Mock一個ZJHTwitterConnection實例
    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一個TweetView實例
    id mockView = OCMClassMock([ZJHTweetView class]);
    controller.tweetView = mockView;
    
    //--------- When Start ---------//
    // 5. 調(diào)用測試目標方法updateTweetView。里面會調(diào)用fetchTweets,然后會得到我們存根的數(shù)組tweetArray
    [controller updateTweetView];
    
    //--------- Then Start ---------//
    // 6. 驗證 mock TweetView 的 addTweet: 顯示Tweet到界面的操作被觸發(fā)
    OCMVerify([mockView addTweet:[OCMArg any]]);
}

@end

2.4、編寫實現(xiàn)代碼

完成了updateTweetView方法的測試用例,就可以大致清楚updateTweetView需要處理什么數(shù)據(jù)(stub)、需要調(diào)用依賴的哪些方法(verify)。此時運行該測試用例必然不通過,因為還未實現(xiàn)updateTweetView。接下來開始實現(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

此時運行測試用例,用例通過,因為滿足了測試用例中的OCMVerify的條件:當(given)connection固定正常返回 Tweet 數(shù)據(jù)時,調(diào)用updateTweetView時(when),觸發(fā)了tweetViewaddTweet:方法顯示 Tweet 數(shù)據(jù)到界面。

三、OCMock的示例使用

3.1、生成 Mock 對象三種方式的對比

3.1.1、需要測試的代碼

通過對Person類的talk方法進行測試舉例,其中也涉及Men類以及Animaiton類,以下是三個類的相關(guān)源碼。

MOPerson類:

@interface MOPerson()
@property(nonatomic,strong) MOMen *men;
@end

@implementation Person
- (void)talk:(NSString *)str {
    [self.men logstr:str];
    [MOAnimaiton logstr:str];
}
@end

MOMen類

@implementation MOMen
-(NSString *)logstr:(NSString *)str {
    NSLog(@"%@",str);
    return str;
}
@end

MOAnimaiton類

@implementation MOAnimaiton
+ (NSString *)logstr:(NSString *)str {
    NSLog(@"%@",str);
    return str;
}
@end
3.1.2、Nice Mock

NiceMock 創(chuàng)建的 mock 對象在進行方法測試時會優(yōu)先調(diào)用實例方法,若未找到實例方法,會繼續(xù)調(diào)用同名的類方法。因此該方法可以用來生成mock對象去測試類方法也可以測試對象方法。使用場景:Nice mock 是比較友好的,當一個沒有存根的方法被調(diào)用時他不會引起一個異常會驗證通過。如果你不想自己對很多的方法進行存根,那么使用 nice mock

- (void)testTalkNiceMock {
    MOPerson *person1 = [MOPerson new]; // 新建person類
    id mockA = OCMClassMock([MOMen class]); // mock一個Men對象
    person1.men = mockA;
    [person1 talk:@"123"];  // person類執(zhí)行方法
    OCMVerify([mockA logstr:[OCMArg any]]); // 驗證 logstr 方法有被調(diào)用
}
3.1.3、Strict Mock

使用方式:測試case如下,mockA是Strict Mock生成,要調(diào)用testTalkStrictMock方法,則該方法要使用stub進行存根,否則最后的OCMVerifyAll(mockA)就會拋出異常。使用場景:這種方式創(chuàng)建的 mock 對象,如果調(diào)用未 stub(stub 代表存根)的方法,會拋出一個異常。這需要保證在 mock 的生命周期中每一個獨立調(diào)用的方法都是被存根的,這種方法使用比較嚴格,很少使用。

- (void)testTalkStrictMock {
    id mockA = OCMStrictClassMock([MOPerson class]); // StrictMock生成mockA
    OCMStub([mockA talk:[OCMArg any]]); // 使用stub進行存根
    [mockA talk:@"123"]; // 執(zhí)行talk方法    
    OCMVerifyAll(mockA); // 驗證mock方法有沒有被執(zhí)行
}
3.1.4、Partial Mock

這樣創(chuàng)建的對象在調(diào)用方法時:如果方法被 stub,調(diào)用 stub 后的方法,如果方法沒有被 stub,調(diào)用原來的對象的方法,該方法有限制只能 mock 實例對象。使用場景:當調(diào)用一個沒有被存根的方法時,會調(diào)用實際對象的該方法。當不能很好的存根一個類的方法時,該技術(shù)是非常有用的。

- (void)testTalkPartialMock {
    MOPerson *person1 = [MOPerson new];
    MOMen *men = [MOMen new];
    id mockA = OCMPartialMock(men);
    // 如果方法被 stub,調(diào)用 stub 后的方法,如果方法沒有被 stub,調(diào)用原來的對象的方法
//    OCMStub([mockA logstr:[OCMArg any]]).andReturn(@"456");;
    person1.men = mockA;
    [person1 talk:@"123"];
    OCMVerify([mockA logstr:[OCMArg any]]);
}

3.2、預期的驗證

3.2.1、需要測試的代碼
@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、驗證預期
- (void)testMockExpect {
    // 新建mock
    id mock = OCMClassMock([MOOCMockDemo class]);

    // 預期下列方法順序執(zhí)行
    [mock setExpectationOrderMatters:YES];
    
    // 預期 + 參數(shù)驗證
    OCMExpect([mock handleLoadSuccessWithPerson:[OCMArg checkWithBlock:^BOOL(id obj) {
        MOPerson *person = (MOPerson *)obj;
        return [person.name isEqualToString:@"momo"];
    }]]);
    // 預期方法
    OCMExpect([mock showError:NO]);

    // 預期不執(zhí)行
    OCMReject([mock handleLoadFailWithPerson:[OCMArg any]]);
    OCMReject([mock showError:YES]).ignoringNonObjectArgs; // 忽視參數(shù)

    // 執(zhí)行方法
    NSDictionary *info = @{@"name": @"momo"};
    [MOOCMockDemo handleLoadFinished:info];
    
    // 斷言
    OCMVerifyAll(mock); // OCMVerifyAll 會驗證前面的期望是否有效,只要有一個沒調(diào)用,就會出錯。
    OCMVerifyAllWithDelay(mock, 1); // 支持延遲驗證
    
    // 停止Mocking
    [mock stopMocking];
}

3.3、網(wǎng)絡(luò)接口的模擬

3.3.1、需要測試的代碼
@implementation ZJHOrderListViewController

- (void)getListData { // 接口請求獲取網(wǎng)絡(luò)數(shù)據(jù)
    [ZJHNetworkTool requestUrl:@"url" param:@{} completion:^(NSDictionary * _Nonnull respondDic) {
        self.dataArr = respondDic[@"data"];
        [self refreshView];
    }];
}

- (void)refreshView { // 刷新頁面
    NSLog(@"***ZJH refreshView : %@", self.dataArr);
}

@end
3.3.2、網(wǎng)絡(luò)接口模擬
- (void)testMockNetwork {
    ZJHOrderListViewController *listVc = [ZJHOrderListViewController new];
    
    id mockManager = OCMClassMock([ZJHNetworkTool class]);
    // mock請求方法,并返回特定參數(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)用接口后模擬需要的返回數(shù)據(jù),successBlock 中的就是返回的測試數(shù)據(jù)。本方式是通過獲取接口調(diào)用的方法簽名,獲取 successBlock 成功回調(diào)傳參并手動調(diào)用。同樣可以模擬接口失敗的情況,只需獲取到簽名中的對應(yīng)的失敗回調(diào)就可以實現(xiàn)了。使用場景:書寫單元測試方法時涉及網(wǎng)絡(luò)接口的模擬,通過該方式 mock 接口返回結(jié)果。

四、OCMock基本API詳解

本章根據(jù)官方文檔 Documentation 改編而來,可以在里查看如何詳細使用OCMock。

4.1、創(chuàng)建模擬對象 Creating mock objects

4.1.1、模擬實例 Class mocks
// 根據(jù)類,模擬其實例
id mockPerson = OCMClassMock([MOPerson class]);
4.1.2、模擬代理 Protocol mocks
// 根據(jù)協(xié)議名,模擬已經(jīng)實現(xiàn)協(xié)議的實例
id mockProtocol = OCMProtocolMock(@protocol(MOTitleLineViewDelegate));
// 然后mock協(xié)議方法
4.1.3、嚴格模擬類和協(xié)議 Strict class and protocol mocks
// 在收到?jīng)]有預期(expect)的方法時引發(fā)異常
id strictMockClass = OCMStrictClassMock([MOPerson class]);
id strictMockProtocol = OCMStrictProtocolMock(@protocol(MOTitleLineViewDelegate));
4.1.4、部分模擬 Partial mocks

這里介紹一個定義:Stub,存根,就是模擬一個函數(shù)。

MOPerson *aPerson = [[MOPerson alloc] init];
id partialMockPerson = OCMPartialMock(aPerson);

調(diào)用一個函數(shù):已經(jīng)存根的就觸發(fā)存根的(Stub);未存根的就觸發(fā)原有實例的(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、委托給另一個方法 Stubbing methods that return values
MOPerson *anotherPerson = [[MOPerson alloc] init];
// 另一個對象的方法,方法簽名需要一致
OCMStub([partialMockPerson name]).andCall(anotherPerson, @selector(name));
4.2.3、委托給一個block Delegating to another method
OCMStub([partialMockPerson name]).andDo(^(NSInvocation *invocation){
    // 調(diào)用name方法時,將會調(diào)用這個block
    // invocation會攜帶方法參數(shù)
    // invocation可以設(shè)置返回值
});
OCMStub([partialMock name]).andDo(nil);
4.2.4、委托給塊 Delegating to a block

模擬對象將在調(diào)用函數(shù)時,調(diào)用該Block。該Block可以從調(diào)用的對象中讀取參數(shù),并可以設(shè)置返回值。

OCMStub([mock someMethod]).andDo(^(NSInvocation *invocation) {
    /* block that handles the method invocation */
});
4.2.5、模擬 通過參數(shù)返回值的方法 的返回值 Returning values in pass-by-reference arguments
4.2.5.1、對象參數(shù)

通過參數(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、非對象參數(shù)
OCMStub([mock someMethodWithReferenceArgument:[OCMArg setToValue:OCMOCK_VALUE((int){aValue})]]);
4.2.6、模擬block參數(shù) Invoking block arguments
// invokeBlock默認模擬,參數(shù)都為默認值
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)用時,拋出異常:

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和 之類的所有操作andPost都可以鏈接

// 模擬對象將發(fā)布通知并返回值
OCMStub([mock someMethod]).andPost(aNotification).andReturn(aValue);
4.2.10、轉(zhuǎn)發(fā)給真正的對象/類 Forwarding to the real object / class

當使用部分模擬實例和模擬類方法時,可以將存根方法轉(zhuǎn)發(fā)給真實對象或類。這僅在鏈接操作或使用期望時有用。

OCMStub([partialMockPerson name]).andForwardToRealObject();
4.2.11、什么也不做 Doing nothing

可以將nil而不是塊傳遞給andDo。這僅在部分模擬或模擬類方法時有用。在這些情況下,使用andDo(nil)有效地抑制了現(xiàn)有類中的行為。

OCMStub([mock someMethod]).andDo(nil);
4.2.12、滿足XCTest的期望(需要OCMock3.8)Fulfilling XCTest expectations

當調(diào)用該方法時,XCTest 框架中的期望得到滿足:

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)用該方法時,format通過NSLog。很可能您想在一個鏈中使用它,可能后跟andReturn()andForwardToRealObject()

4.2.14、打開調(diào)試,斷點會生效(需要OCMock3.8)
OCMStub([partialMockPerson name]).andBreak();

當調(diào)用該方法時,調(diào)試器被打開,就好像一個斷點被命中一樣。堆棧將在 OCMock 的實現(xiàn)中的某個地方結(jié)束,但是如果您進一步查看,越過__forwarding__幀,您應(yīng)該能夠看到您的代碼調(diào)用該方法的位置。

4.3、驗證交互 Verifying interactions

4.3.1、驗證方法已調(diào)用 Verify-after-running
[aPerson name];
OCMVerify([partialMockPerson name]);

驗證name已被測試代碼調(diào)用。如果尚未調(diào)用該方法,則會報告錯誤。

4.3.2、驗證Stubbed的方法被調(diào)用 Stubs and verification
OCMStub([partialMockPerson name]).andReturn(@"momo");
[aPerson name];
OCMVerify([partialMockPerson name]);

可以存根一個方法并仍然驗證它是否已被調(diào)用。

4.3.3、量詞要求 Quantifiers requires

驗證方法被調(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ù)是任何對象
OCMStub([partialMockPerson takeMoney:[OCMArg anyPointer]]); // 參數(shù)是任何指針
OCMStub([partialMockPerson changeWithSelector:[OCMArg anySelector]]); // 參數(shù)是任何選擇子
4.4.2、忽視沒有對象參數(shù) Ignoring non-object arguments

stub方法,可以響應(yīng)非對象參數(shù)的調(diào)用(可以響應(yīng)參數(shù)沒有通過的調(diào)用:無論是對象參數(shù) or 非對象參數(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]]]);

會觸發(fā) anObjectaSelector 方法,并將參數(shù)傳入
在該方法中判斷參數(shù)是否通過,通過就:返回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ù)是否通過,通過就:返回YES, 否則:返回NO
    return YES;
}]]);
4.4.4、使用Hamcrest匹配
OCMStub([partialMockPerson addChilden:startsWith(@"foo")]);

4.5、模擬類方法 Mocking class methods

4.5.1、存根類方法 Stubbing class methods
id mockPerson = OCMClassMock([MOPerson class]);
OCMStub([mockPerson mo_className]).andReturn(@"XXMOPerson");
4.5.2、消除類和實例方法的歧義 Disambiguating class and instance methods
// (1)此時如果沒有同名的實例方法,mo_className類方法是可以被正確Stub的
NSString *className1 = [MOPerson mo_className]; // XXMOPerson
// (2)但是如果實例方法有跟之同名時:
NSString *instanceName = [mockPerson mo_className]; // XXMOPerson
NSString *className2 = [MOPerson mo_className]; // class MOPerson
// 則需要用一下方法進行Stub
OCMStub(ClassMethod([mockPerson mo_className])).andReturn(@"MOMOPerson");
NSString *className3 = [MOPerson mo_className]; // XXMOPerson
4.5.3、驗證類方法已執(zhí)行 Verifying invocations of class methods
[mockPerson mo_className];
OCMVerify([mockPerson mo_className]);
4.5.4、恢復類 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、驗證調(diào)用 Verifying invocations
[partialMockPerson mo_className];
OCMVerify([partialMockPerson mo_className]);
4.6.3、恢復對象 Restoring the object
[partialMockPerson stopMocking];

4.7、嚴格的模擬和期望 Strict mocks and expectations

4.7.1、設(shè)置期望-運行-驗證 Expect-run-verify
id mockPerson = OCMClassMock([MOPerson class]);
OCMExpect([mockPerson addChilden:[OCMArg isNotNil]]);
[mockPerson addChilden:[MOPerson new]]; // 只要有一次不為nil,就通過了驗證!
[mockPerson addChilden:nil];
OCMVerifyAll(mockPerson);
4.7.2、嚴格的模擬和快速失敗 Strict mocks and failing fast
id strictPerson = OCMStrictClassMock([MOPerson class]);
[strictPerson mo_className]; // 沒有期望該方法的調(diào)用,所以會測試失敗
4.7.3、存根和期望 Stub actions and expect

也可以在期望的情況下使用andReturn、andThrow等。這將在調(diào)用方法時運行存根操作,并在驗證時確保該方法被實際調(diào)用

OCMExpect([strictPerson mo_className]).andReturn(@"instance_MOPerson");
OCMExpect([strictPerson mo_className]).andThrow([NSException ...]);
[strictPerson mo_className];
OCMVerifyAll(strictPerson);
4.7.4、延遲驗證 Verify with delay
OCMExpect([strictPerson mo_className]);
[strictPerson mo_className];
OCMVerifyAllWithDelay(strictPerson, 4.0); // NSTimeInterval, 通常會在滿足預期后立即返回
4.7.5、按順序驗證 Verifying in order

一旦調(diào)用了不在“預期列表”中的下一個方法,模擬就會快速失敗并拋出異常。

[strictPerson setExpectationOrderMatters:YES];
OCMExpect([strictPerson mo_className]);
OCMExpect([strictPerson addChilden:[OCMArg any]]);
// 調(diào)用順序錯了,測試就會失敗
[strictPerson mo_className];
[strictPerson addChilden:nil];

4.8、觀察者模擬 Observer mocks

OCMock 4.8開始不推薦使用觀察者模擬。請改用XCTNSNotificationExpectation

4.9、進階主題 Advanced topics

4.9.1、快速失敗的常規(guī)模擬 (需要OCMock3.3) Failing fast for regular (nice) mocks

strict模擬:調(diào)用未存根的方法會拋出異常
常規(guī)模擬:只是返回默認值;可以為函數(shù)配置快速失敗:

id mockPerson = OCMClassMock([MOPerson class]);
OCMReject([mockPerson mo_className]);

在這種情況下,模擬將接受所有方法,除了mo_className,如果調(diào)用該函數(shù),則將引發(fā)異常。

4.9.2、重新驗證失敗后快速拋出異常 Re-throwing fail fast exceptions in verify all

在快速失敗模式下,異常可能不會導致測試失?。ㄈ纾寒敺椒ǖ恼{(diào)用堆棧未在測試中結(jié)束時)
OCMerifyAll調(diào)用時,快速失敗異常將重新引發(fā),可以確保檢測到來自通知等不需要的調(diào)用

4.9.3、存根創(chuàng)建對象的方法 Stubbing methods that create objects
MOPerson *myPerson = [[MOPerson alloc] init];
OCMStub([mockPerson copy]).andReturn(myPerson);

會根據(jù)方法名,自動返回對象的:alloc、new、copy、mutableCopy (引用計數(shù))

注意:init方法無法Stub,因為該方法是由模擬本身實現(xiàn)的。 當init方法再次被調(diào)用時,會直接返回模擬對象self
這樣就可以有效的對alloc、init進行Stub

4.9.4、基于實現(xiàn)的方法交換 Instance-based method swizzling
MOPerson *person = [[MOPerson alloc] init];
id partialMockPerson = OCMPartialMock(person);
OCMStub([partialMockPerson mo_className]).andCall(myPerson, @selector(name));

方法的名稱可以不同,但是簽名應(yīng)該相同

4.9.5、打破保留周期 Breaking retain cycles
[mockPerson stopMocking];
[partialMockPerson stopMocking];
4.9.6、禁用短語法 Disabling short syntax

禁用 沒有前綴的宏:ClassMethod()、atLeast()、…
用有前綴的宏:OCMClassMethod()OCMAtLeast()、…

4.9.7、停止為特定類創(chuàng)建模擬 (需要OCMock3.8) Stopping creation of mocks for specific classes

一些框架在運行時動態(tài)更改對象的類。OCMock這樣做是為了實現(xiàn)部分模擬,并且Foundation框架將更改類作為(KVO)機制的一部分。
如果不仔細協(xié)調(diào),可能會導致意外行為或crash。

OCMock知道KVO,并小心避免與之發(fā)生沖突
對于其它框架,OCMock僅提供了一種選擇退出模擬以免發(fā)生意外行為的機制

+ (BOOL)supportsMocking:(NSString **)reason {
    *reason = @"Don't want to be mocked."
    return NO;
}

通過實現(xiàn)上面的方法,一個類可以選擇不被Mock。當開發(fā)人員嘗試為此類創(chuàng)建模擬程序時,將引發(fā)異常,解釋問題說在該方法在單獨調(diào)用中返回不同的值是可以接受的,這使它在運行時對特定條件做出反應(yīng)。如果該方法為reason賦值,返回值將被忽略。對于所有未實現(xiàn)此方法的類,OCMock假定可以接受Mock

4.9.8、檢查部分Mock (需要OCMock3.8) Checking for partial mock

判斷是否 是部分模擬對象

BOOL isPartialMockObj = OCMIsSubclassOfMockClass(objc_getClass(partialMockPerson));

4.10、局限性 Limitations

4.10.1、一次只能有一個Mock可以在給定類上存根方法

不要這樣做:

id mock1 = OCMClassMock([SomeClass class]);
OCMStub([mock1 aClassMethod]);
id mock2 = OCMClassMock([SomeClass class]);
OCMStub([mock2 anotherClassMethod]);

如果添加了存根類方法的模擬對象未釋放,則存根方法將持續(xù)存在,即使在測試中也是如此。如果多個模擬對象同時操作同一類,則行為將不可預測。

4.10.2、期望Stub方法無效
id mock = OCMStrictClassMock([SomeClass class]);
OCMStub([mock someMethod]).andReturn(@"a string");
OCMExpect([mock someMethod]);

由于當前實現(xiàn)了模擬對象的方法,Stub會處理所有對它的調(diào)用。意味著即使調(diào)用了該方法,驗證也會失敗。避免此問題:

  • 方法1:通過andReturnExpect語句中添加
  • 方法2:在設(shè)置期望之后存根
4.10.3、不能為某些特殊類創(chuàng)建部分模擬
id partialMockForString = OCMPartialMock(@"Foo"); // 會拋出異常

NSDate *date = [NSDate dateWithTimeIntervalSince1970:0];
id partialMockForDate = OCMPartialMock(date); // 會對一些架構(gòu)造成影響嗎

無法為 toll-free bridged 類的實例創(chuàng)建局部模擬
無法為 某些實例創(chuàng)建以標記指針表示的對象,如:NSString、在某些體系結(jié)構(gòu)上、NSDate在某些體系結(jié)構(gòu)上

4.10.4、某些方法無法存根或驗證
id partialMockForString = OCMPartialMock(anObject);
OCMStub([partialMock class]).andReturn(someOtherClass); // will not work

無法模擬許多核心運行時方法。包括:init、class、methodSignatureForSelector:、forwardInvocation:、respondsToSelector等等

4.10.5、NSString和NSArray上的類方法無法存根或驗證
// 無法生效、該方法將不會被存根
id stringMock = OCMClassMock([NSString class]);
// 無法在NSString和NSArray上存根或驗證類方法。嘗試這樣做沒有任何效果。
OCMStub([stringMock stringWithContentsOfFile:[OCMArg any] encoding:NSUTF8StringEncoding error:[OCMArg setTo:nil]]);
4.10.6、NSManagedObject的類方法及其子類無法存根或驗證
// 無法生效、該方法將不會被存根
id mock = OCMClassMock([MyManagedObject class]);
// 無法在其NSManagedObject或其子類上存根或驗證類方法。嘗試這樣做沒有任何效果。
OCMStub([mock someClassMethod]).andReturn(nil);
4.10.7、無法驗證 NSObject 上的方法
id mock = OCMClassMock([NSObject class]);
/* run code under test, which calls awakeAfterUsingCoder: */
OCMVerify([mock awakeAfterUsingCoder:[OCMArg any]]); // still fails

不可能使用在 NSObject 中實現(xiàn)的方法或其上的類別進行運行后驗證。
在某些情況下,可以對方法進行存根,然后對其進行驗證。
當方法在子類中被覆蓋時,可以使用運行后驗證

4.10.8、無法驗證核心 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

不可能在核心 Apple 類中使用私有方法運行后驗證。
具體來說,在以 NSUI 作為前綴的類中,所有帶有下劃線前綴和/或后綴的方法。
在某些情況下,可以對方法進行存根,然后對其進行驗證

4.10.9、運行后驗證不能使用延遲

目前無法驗證具有延遲的方法。這目前只能使用下面在嚴格模擬和期望中描述的expect-run-verify方法。

4.10.10、測試中使用多線程

OCMock 不是完全線程安全的。直到 4.2.x 版本 OCMock 根本不知道線程。來自多個線程的模擬對象上的任何操作組合都可能導致問題并使測試失敗。

OCMock 4.3 開始,仍然需要從單個線程調(diào)用所有設(shè)置和驗證操作,最好是測試運行程序的主線程。
但是,可以從多個線程使用模擬對象。模擬對象甚至可以在不同的線程中使用,而其設(shè)置在主線程中繼續(xù)進行。

五、部分補充

5.1、單例的mock

不能直接mock單例的,會引起mock沖突。推薦的寫法:

// 每次mock alloc 一個單例
id center = OCMPartialMock([[QLLoginCenter alloc] init]); 
// mock 它的 sharedInstance 方法
OCMStub([[center classMethod] sharedInstance]).andReturn(center); 



參考鏈接:
iOS中的測試:OCMock:http://m.itdecent.cn/p/44ea034ac755
iOS_單元測試三之OCMock使用:https://blog.csdn.net/Margaret_MO/article/details/115420007
iOS_單元測試三之OCMockDemo:https://blog.csdn.net/Margaret_MO/article/details/118341525
iOS 單元測試之常用框架 OCMock 詳解:https://www.51cto.com/article/707544.html
iOS單元測試-06-OCMoke和Stub詳解:http://m.itdecent.cn/p/6fd98f95d1ba

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

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

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