The blog link

In object-oriented programming, there is an interesting concept called duck type, which means that if something walks like a duck, swims like a duck, and quacks like a duck, it can be considered a duck. This means that when we need a duck object, we can instantiate it by either instantiation or 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];
Copy the code

The latter defines a set of duck interfaces to imitate a Duck Type object. Although the object is simulated, it does not prevent the normal execution of the program. This design idea can be called mock

Test or debug program functionality by making fake objects that mimic the behavior of real objects

Interface and the mock

Although the mock effect is achieved through the interface design above, the two are not equal. In terms of design, an interface abstracts a set of behavior interfaces or attributes, and does not care whether implementers have specific implementation differences. A mock requires that both the mock object and the real object have the same behavior and properties, and a consistent implementation of the behavior:

/// A test engineer enters a bar and orders a beer. A development engineer enters a cafe and orders a cup of coffeeCopy the code

In terms of implementation, although interface can achieve 100% restoration of the real object by abstracting all the behaviors and attributes of the real object, it violates the principle that interface should only provide a series of interfaces with the same function. Therefore, interface is more suitable for module decoupling and function extension related work. As mock requires the mock object to copy the real object 100%, it is more used in debugging, testing and other aspects of work

How to Implement mocks

Personally, MOCK can be divided into behavioral simulation and complete simulation according to the degree of simulation. There are altogether four methods for simulating real objects:

  • inherit
  • interface
  • forwarding
  • isa_swizzling

Behavior modeling

Behavior simulation seeks to restore the core behavior of real objects. Due to the message processing mechanism of OC, both interface extension of interface and forwarding processing of forwarding can complete the simulation of real objects:

/// 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
Copy the code

The difference between interface and forwarding is that the real handler of the latter can be the real object itself. However, since forwarding does not necessarily need to be forwarded to the real object for processing, the two can be either behavior simulation or complete simulation. But more often, they are duck types

Complete simulation

Complete simulation requires the imitation of the real, in any case the simulated object can behave no different from the real object:

@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];
Copy the code

Although there is no difference in the behavior of inherit and ISA_swizzling, the latter is more like borrowing all the attributes and structures of the subclass and only displaying the behavior of Duck. However, as there is no real isa_swizzling object in the unit test, the mock object needs to be dynamically generated to complete the mock object construction:

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];
Copy the code

Structure simulation

Structural emulation is a mock that is as powerful as it is destructive. Since data structures are ultimately stored in binary, structural emulation attempts to build the binary structure layout of the entire real object and then modify the variables within the structure. At the same time, structural simulation is not required to master the exact layout of the object, as long as we know the location of the data we need to modify. For example, an OC block is actually a variable length structure. The size of the structure increases with the number of captured variables, but the first 32 bits of stored information are fixed. Its structure is as follows:

struct Block { void *isa; int flags; int reserved; void (*invoke)(void *, ...) ; struct BlockDescriptor *descriptor; /// catched variables };Copy the code

The invoke pointer points to the address of the IMP function and changes the behavior of the block by modifying this pointer value:

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();Copy the code

Mocking the structural layout of real objects to capture or even modify the behavior of real objects is powerful, but if the structural layout of objects is different because of a different version of the system, or if the layout information obtained is inaccurate, the data itself can be corrupted, leading to unexpected errors

When to mock

From personal development experience, we can consider using mocks to replace real objects if:

  • Type missing running environment
  • The result depends on the asynchronous operation
  • Real objects are not visible to the outside world

The first two are more likely to occur in unit testing, while the latter are more likely to be related to debugging

Type missing running environment

NSUserDefaults stores the data in the corresponding key-value format in the sandbox directory. However, in the unit test environment, the program is not programmed into binary packages, so NSUserDefaults cannot be used properly, so you can use mock to restore the test scenario. OCMock is usually chosen to fulfill the mock requirement for unit tests:

- (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];
Copy the code

In fact, the IO classes associated with sandboxes were virtually unavailable in unit testing, so data structures such as mock are a good way to support sandbox storage

The result depends on the asynchronous operation

XCTAssert provides a delay interface for asynchronous operations, but of course it does not work. Asynchronous processing is often the killer of unit testing, and OCMock also provides interface support for asynchrony:

- (void)requestWithData: (NSString *)data complete: (void(^)(NSDictionary *response))complete; OCMStub([requestMock requestWithData: @"xxxx" complete: [OCMArg any]]). Invocation (^(NSDictionary *response) {/// Invocation (NSDictionary *response);  [invocation getArgument: &complete atIndex: 3]; NSDictionary *response = @{ @"success": @NO, @"message": @"wrong data" }; complete(response); });Copy the code

In addition to existing third-party tools, it is also possible to implement a tool to handle asynchronous testing through message forwarding:

@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 }];Copy the code

The final stage of the relay wraps the message as an NSInvocation object, which provides the invocation invocation to walk through the invocation parameters, determine the parameter type with @encode(), get the callback block and call

Real objects are not visible to the outside world

There are two situations in which the real object is not visible:

  • Structure not visible
  • None of the structure instances are visible

In almost all cases, the structure is invisible, such as private classes, private structures, etc. The block structure mentioned above is the most obvious example. By overriding the class file with the clang command, you can basically get the inner structure of such objects. Since we’ve already shown a layout simulation of the block, we won’t go into more detail here

clang -rewrite-objc xxx.m
Copy the code

The latter is special, and we can’t get either the structural layout or the instance object. For example, I need to count the binary segment of the application compilation package. Using the Hopper tool, I can get the objc_classlist_DATA segment:

Since there is no real object or structure reference at this point, all we know is that each __objc_data is 72 bytes long. Therefore, in this case, it is necessary to first simulate a structure of equal length than binary data, and then output hexadecimal data to match the layout information of the data segment:

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: @"  "]);
Copy the code

By analyzing hexadecimal segment data and combining the data segment information obtained by Hopper, the layout information of real objects can be drawn, and then the simulated structure can be constructed by structural simulation:

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); . }Copy the code

The above code has been simplified. In fact, traversing machO requires loading binaries into memory, considering the offset between the hopper load and your own manual load, and finally figuring out the correct address value. In the whole traversal process, except for header and command structures exposed by the system, other stored objects need to check hopper and base values for derivation, and finally mock out the structure to complete the work

conclusion

Mock is not a specific manipulation or programming technique, but rather a way of dissecting engineering details to solve problems in a particular environment. In any case, mock is definitely a good idea to learn from more perspectives and tools to understand and solve development problems if we want to continue to explore the development world, right