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绝对是一种值得学习的开发思想

关注个人公众号获取更新信息
相关文章
相关标签/搜索