mock in iOS

博客連接html

在面向對象編程中,有個很是有趣的概念叫作duck type,意思是若是有一個走路像鴨子、游泳像鴨子,叫聲像鴨子的東西,那麼它就能夠被認爲是鴨子。這意味着當咱們須要一個鴨子對象時,能夠經過instantiation或者interface兩種機制來提供鴨子對象:編程

@interface Duck : NSObject

@property (nonatomic, assign) CGFloat weigh;

- (void)walk;
- (void)swim;
- (void)quack;

@end

/// instantiation
id duckObj = [[Duck alloc] init];
[TestCase testWithDuck: duckObj];

/// interface
@protocol DuckType

- (void)walk;
- (void)swim;
- (void)quack;
- (CGFloat)weigh;
- (void)setWeigh: (CGFloat)weigh;

@end

@interface MockDuck : NSObject<DuckType>
@end

id duckObj = [[MockDuck alloc] init];
[TestCase testWithDuck: duckObj];
複製代碼

後者定義了一套鴨子接口,模仿出了一個duck type對象,雖然對象是模擬的,但這並不阻礙程序的正常執行,這種設計思路,能夠被稱做mock數據結構

經過製造模擬真實對象行爲的假對象,來對程序功能進行測試或調試app

interface和mock

雖然上面經過interface的設計實現了mock的效果,但二者並不能劃上等號。從設計思路上來講,interface是抽象出一套行爲接口或者屬性,且並不關心實現者是否存在具體實現上的差別。而mock須要模擬對象和真實對象二者具備相同的行爲和屬性,以及一致的行爲實現:異步

/// interface
一個測試工程師進了一間酒吧點了一杯啤酒
一個開發工程師進了一間咖啡廳點了一杯咖啡

/// mock
一個測試工程師進了一間酒吧點了一杯啤酒
一個模擬的測試工程師進了一間酒吧點了一杯啤酒
複製代碼

從實現上來講,雖然interface能夠經過抽象出真實對象全部的行爲和屬性來完成對真實對象的百分百還原,但這樣就違背了interface應只提供一系列相同功能接口的原則,所以interface更適用於模塊解耦、功能擴展相關的工做。而mock因爲要求模擬對象對真實對象百分百的copy,更多的應用在調試、測試等方面的工做函數

如何實現mock

我的把mock根據模擬程度分爲行爲模擬和徹底模擬兩種狀況,對於真實對象的模擬,總共包括四種方式:工具

  • inherit
  • interface
  • forwarding
  • isa_swizzling

行爲模擬

行爲模擬追求的是對真實對象的核心行爲進行還原。因爲OC的消息處理機制,所以不管是interface的接口擴展仍是forwarding的轉發處理均可以完成對真實對象的模擬:佈局

/// interface
@interface InterfaceDuck : NSObject<DuckType>
@end

/// forwarding
@interface ForwardingDuck : NSObject

@property (nonatomic, strong) Duck *duck;

@end

@implementation MockDuck

- (id)forwardingTargetForSelector: (SEL)selector {
    return _duck;
}

@end
複製代碼

interfaceforwarding的區別在於後者的真正處理者能夠是真實對象自己,不過因爲forwarding不必定非要轉發給真實對象處理,因此兩者既能夠是行爲模擬,也能夠是徹底模擬。但更多時候,二者是duck type單元測試

徹底模擬

徹底模擬要求以假亂真,在任何狀況下模擬對象能夠表現的跟真實對象無差異化:學習

@interface MockDuck : Duck
@end

/// inherit
MockDuck *duck = [[MockDuck alloc] init];
[TestCase testWithDuck: duck];

/// isa_swizzling
Duck *duck = [[Duck alloc] init];
object_setClass(duck, [MockDuck class]);
[TestCase testWithDuck: duck];
複製代碼

雖然inheritisa_swizzling兩種方式的行爲沒有任何差異,可是後者更像是借用了子類的全部屬性、結構,而只呈現Duck的行爲。但在單元測試中的mock,因爲並不存在直接進行isa_swizzling的真實對象,還須要動態的生成class來完成模擬對象的構建:

Class MockClass = objc_allocateClassPair(RealClass, RealClassName, 0);
objc_registerClassPair(MockClass);

for (Selector s in getClassSelectors(RealClass)) {
    Method m = class_getInstanceMethod(RealClass, s);
    class_addMethod(MockClass, s, method_getImplementation(m), method_getTypeEncoding(m));
}
id mockObj = [[MockClass alloc] init];
[TestCase testWithObj: mockObj];
複製代碼

結構模擬

結構模擬是一種威力和破壞能力一樣強大的mock方式,因爲數據結構最終採用二進制存儲,結構模擬嘗試構建整個真實對象的二進制結構佈局,而後修改結構內變量。同時,結構模擬並不要求必須掌握對象的準確佈局信息,只要清楚咱們須要修改的數據位置就好了。譬如OCblock其實是一個可變長度的結構體,結構體的大小會隨着捕獲的變量數量增大,可是前32位的存儲信息是固定的,其結構體以下:

struct Block {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct BlockDescriptor *descriptor;
    /// catched variables
};
複製代碼

其中invoke指針指向了其imp的函數地址,只要修改這個指針值,就能改變block的行爲:

struct MockBlock {
    ...
};

void printHelloWorld(void *context) {
    printf("hello world\n");
};

dispatch_block_t block = ^{
    printf("I'm block!\n");
};
struck MockBlock *mock = (__bridge struct MockBlock *)block;
mock->invoke(NULL);
mock->invoke = printHelloWorld;
block();
複製代碼

經過mock真實對象的結構佈局來獲取真實對象的行爲,甚至修改行爲,雖然這種作法很是強大,但若是由於系統版本的差別致使對象的結構佈局存在差別,或者獲取的佈局信息並不許確,就會破壞數據自己,致使意外的程序錯誤

何時用mock

從我的開發經從來看,若是有如下狀況,咱們能夠考慮使用mock來替換真實對象:

  • 類型缺失運行環境
  • 結果依賴於異步操做
  • 真實對象對外不可見

其中前二者更多發生在單元測試中,然後者多與調試工做相關

類型缺失運行環境

NSUserDefaults會將數據以key-value的對應格式存儲在沙盒目錄下,但在單元測試的環境下,程序並無編程成二進制包,所以NSUserDefaults沒法被正常使用,所以使用mock能夠還原測試場景。一般會選擇OCMock來完成單元測試的mock需求:

- (void)testUserDefaultsSave: (NSUserDefaults *)userDefaults {
      [userDefaults setValue: @"value" forKey: @"key"];
      XCTAssertTrue([[userDefaults valueForKey: @"key"] isEqualToString: @"value"])
}

id userDefaultsMock = OCMClassMock([NSUserDefaults class]);
OCMStub([userDefaultsMock valueForKey: @"key"]).andReturn(@"value");
[self testUserDefaultsSave: userDefaultsMock];
複製代碼

實際上在單元測試中,與沙盒相關的IO類都幾乎處於不可用狀態,所以mock這樣的數據結構能夠很好的提供對沙盒存儲功能的支持

結果依賴於異步操做

XCTAssert爲異步操做提供了一個延時接口,固然並無卵用。異步處理每每是單元測試的殺手,OCMock一樣提供了對於異步的接口支持:

- (void)requestWithData: (NSString *)data complete: (void(^)(NSDictionary *response))complete;

OCMStub([requestMock requestWithData: @"xxxx" complete: [OCMArg any]]).andDo(^(NSInvocation *invocation) {
    /// invocation是request方法的封裝
    void (^complete)(NSDictionary *response);
    [invocation getArgument: &complete atIndex: 3];

    NSDictionary *response = @{
                              @"success": @NO,
                              @"message": @"wrong data"
                              };
    complete(response);
});
複製代碼

拋開已有的第三方工具,經過消息轉發機制也能夠實現一個處理異步測試的工具:

@interface AsyncMock : NSProxy {
    id _callArgument;
}

- (instancetype)initWithAsyncCallArguments: (id)callArgument;

@end

@implementation AsyncMock

- (void)forwardInvocation: (NSInvocation *)anInvocation {
    id argument = nil;
    for (NSInteger idx = 2; idx <anInvocation.methodSignature.numberOfArguments; idx++) {
        [anInvocation getArgument: &argument atIndex: idx];
        if ([[NSString stringWithUTF8String: @encode(argument)] hasPrefix: @"@?"]) {
            break;
        }
    }
    if (argument == nil) {
        return;
    }

    void (^block)(id obj)  = argument;
    block(_callArgument;)
}

@end

NSDictionary *response = @{
                          @"success": @NO,
                          @"message": @"wrong data"
                          };
id requestMock = [[AsyncMock alloc] initWithAsyncCallArgument: response];
[requestMock requestWithData: @"xxxx" complete: ^(id obj) {
    /// do something when request complete
}];
複製代碼

轉發的最後一個階段會將消息包裝成NSInvocation對象,invocation提供了遍歷獲取調用參數的信息,經過@encode()對參數類型進行判斷,獲取回調block而且調用

真實對象對外不可見

真實對象對外不可見存在兩種狀況:

  • 結構不可見
  • 結構實例均不可見

幾乎在全部狀況下咱們遇到的都是結構不可見,好比私有類、私有結構等,上文中提到的block結構體就是最明顯的例子,經過clang命令重寫類文件基本能夠獲得這類對象的結構內部。因爲上文已經展現過block的佈局模擬,這裏就再也不多說

clang -rewrite-objc xxx.m
複製代碼

然後者比較特殊,不管是結構佈局,仍是實例對象,咱們都沒法獲取到。打個比方,我須要統計應用編譯包的二進制段的信息,經過使用hopper工具能夠獲得objc_classlist_DATA段的狀況:

因爲此時沒有任何的真實對象和結構參考,只能知道每個__objc_data的長度是72字節。所以這種狀況下須要先模擬出等長於二進制數據的結構體,而後經過輸出16進制數據來匹配數據段的佈局信息:

struct __mock_binary {
    uint vals[18];
};

NSMutableArray *binaryStrings = @[].mutableCopy;
for (int idx = 0; idx <18; idx++) {
    [binaryStrings appendString: [NSString stringWithFormat: @"%p", (void *)binary->vals[idx]]];
}
NSLog(@"%@", [binaryStrings componentsJoinedByString: @"  "]);
複製代碼

經過分析16進制段數據,結合hopper得出的數據段信息,能夠繪製出真實對象的佈局信息,而後採用結構模擬的方式構建模擬的結構體:

struct __mock_objc_data {
    uint flags;
    uint start;
    uint size;
    uint unknown;
    uint8_t *ivarlayouts;
    uint8_t *name;
    uint8_t *methods;
    uint8_t *protocols;
    uint8_t *ivars;
    uint8_t *weaklayouts;
    uint8_t *properties;
};

struct __mock_objc_class {
    uint8_t *meta;
    uint8_t *super;
    uint8_t *cache;
    uint8_t *vtable;
    struct __mock_objc_data *data;
};

struct load_command *cmds = (struct load_command *)sizeof(struct mach_header_64);
for (uint idx = 0; idx <header.ncmds; idx++, cmds = (struct load_command *)(uint8_t *)cmds + cmds->cmdsize) {
    struct segment_command_64 *segCmd = (struct segment_command_64 *)cmds;
    struct section_64 *sections = (struct section_64 *)((uint8_t *)cmds +sizeof(struct segment_command_64));

    uint8_t *secPtr = (uint8_t *)section->offset;
    struct __mock_objc_class *objc_class = (struct __mock_objc_class *)secPtr;
    struct __mock_objc_data *objc_data = objc_class->data;
    printf("%s in objc_classlist_DATA\n", objc_data->name);
    ......
}
複製代碼

上述代碼已作簡化展現。實際上遍歷machO須要將二進制文件載入內存,還要考慮hopper加載跟本身手動加載的地址偏移差,最終求出一個正確的地址值。在整個遍歷過程當中,除了headercommand等結構是系統暴露的以後,其餘存儲對象都須要去查看hopper加上進制數值進行推導,最終mock出結構完成工做

總結

mock並非一種特定的操做或者編程手段,它更像是一種剖析工程細節來解決特殊環境下難題的解決思路。不管如何,若是咱們想要繼續在開發領域上繼續深刻,必然要學會從更多的角度和使用更多的工具來理解和解決開發上的難題,而mock絕對是一種值得學習的開發思想

關注個人公衆號獲取更新信息
相關文章
相關標籤/搜索