Underneath OCMock 🧱

2022, May 13    

OCMock is one of the well known representitive frameworks which can demonstrate the powerfulness of the Objective-C runtime. Recently I was working on choosing a suitable testing frameworks for iOS projects, so I took the chance to throughly read through its source code.

1. Key Points

The following key techniques are used to implement the ObjC mocking via the languages runtime features:

(1) Creating class & class/instance method during runtime. In OCMock, when invoking the [OCMClassMock xxxxClass], the OCMock framework scans the class meta and create subclasses for the original class, and bind the class to its the new meta, hence the invocations are forwarded to the new meta class. In the meanwhile, extra methods are added in the new meta class and existing methods in meta class are also replaced with new implementations. Some of the code snippets are demonstrated below:

  • Create new class methods, replace existing class meta:
// in OCMClassMockObject.m

/* dynamically create a subclass and use its meta class as the meta class for the mocked class */
classCreatedForNewMetaClass = OCMCreateSubclass(mockedClass, mockedClass);
originalMetaClass = object_getClass(mockedClass);
id newMetaClass = object_getClass(classCreatedForNewMetaClass);

/* create a dummy initialize method */
Method myDummyInitializeMethod = class_getInstanceMethod([self mockObjectClass], @selector(initializeForClassObject));
const char *initializeTypes = method_getTypeEncoding(myDummyInitializeMethod);
IMP myDummyInitializeIMP = method_getImplementation(myDummyInitializeMethod);

/* assign initialize selector to [OCMClassMockObject super] which is OCMMockObject's meta class */ 
class_addMethod(newMetaClass, @selector(initialize), myDummyInitializeIMP, initializeTypes);
object_setClass(mockedClass, newMetaClass);
  • Overriding meta class to a existing class instance and then override the forwardInvocation method.
// in OCMPartialMockObject.m

/* dynamically create a subclass and set it as the class of the object */
Class subclass = OCMCreateSubclass(mockedClass, realObject);
object_setClass(realObject, subclass);

/* point forwardInvocation: of the object to the implementation in the mock */
Method myForwardMethod = class_getInstanceMethod([self mockObjectClass], @selector(forwardInvocationForRealObject:));
IMP myForwardIMP = method_getImplementation(myForwardMethod);
class_addMethod(subclass, @selector(forwardInvocation:), myForwardIMP, method_getTypeEncoding(myForwardMethod));

(2) Forwarding unrecognized selectors and match it with stubings. Compares to the first point, this is more well known to iOS community. The invocation sequence between resolvingInstanceMethod, forwardTargetForSelector, methodSignatureForSelector, forwardInvocation can be summarized into the diagram below:

2. Invocation Sequences

2.1 Mocking

id classMock = OCMClassMock([GDZOSTestDemo class]);

Since OCMock supports both full mock and partial mock, in this blog we will focus on elaborating on how the full mock works. When [OCMClassMock xxxClass] is invoked, the call eventually triggers [OCClassMockObject initWithClass] method, and internally it conducts the following steps to create the full mock object.

  • (1) Check if there is an mock already created for the mockClass, for a class with existing mock, there should be a existing mock assigned to OCMClassMethodMockObjectKey in the origin class.
  • (2) Check if the class can be mocked, classes like NSArray and NSString can not be mocked to avoid loops.
  • (3) Dynamically create a subclass and use its meta class(here we call it as Meta-A) as the meta class for the mocked class
  • (4) Create dummy initialize method for the meta class Meta-A. the initialize method for Meta-A is a empty implementation, I guess it is used to avoid triggering those customized initialization logic in the original class.
  • (5) Override the Meta-A’s forwardInvocation method, all class method invocation will be routed to the specified method.
 /* point forwardInvocation: of the object to the implementation in the mock */
Method myForwardMethod = class_getInstanceMethod([self mockObjectClass], @selector(forwardInvocationForClassObject:));
IMP myForwardIMP = method_getImplementation(myForwardMethod);
class_addMethod(newMetaClass, @selector(forwardInvocation:), myForwardIMP, method_getTypeEncoding(myForwardMethod));
  • (6) For the instance method mock, the instance of OCMClassMockObject will rely on the OCMockObject’s forwardingTargetForSelector method for localised check.

2.2 Stubing

OCMStub([classMock sayHi]).andReturn(@”hello”);

Now after the mock instance is created, the next step is to setup those relevant stubbing for the instance. The stub recorder is created via the following macro:

#define OCMStub(invocation) \
({ \
    _OCMSilenceWarnings( \
        [OCMMacroState beginStubMacro]; \
        OCMStubRecorder *recorder = nil; \
        @try{ \
            invocation; \
        }@catch(...){ \
            [[OCMMacroState globalState] setInvocationDidThrow:YES]; \
            /* NOLINTNEXTLINE(google-objc-avoid-throwing-exception) */ \
            @throw; \
        }@finally{ \
            recorder = [OCMMacroState endStubMacro]; \
        } \
        recorder; \
    ); \
})

Explaination of the code above:

  • [OCMMacroState beginStubMacro] will create a macroState and bind it to the current working thread, inside a macroState object, there will be a StubRecorder.
  • now invoke the invocation, the invocation will be intercepted by the [OCMockObject forwardingTargetForSelector], during the creation of the macroState, the invocation will be further forwarded to the StubRecorder , the stubRecorder will add its InvocationMatcher to the mockObject’s stub lists.
  • now after the second step, all the andReturn will be directly calling on the OCMStubRecorder and further create a ValueProvider instance to return the expected value for the invocation.

The following code snippet shows how the macro andReturn eventually executes and creates the value provider for the invocation on a mock instance.

// in OCMStubRecorder.m

- (id)andReturn:(id)anObject
{
    id action;
    if(anObject == mockObject)
    {
        action = [[[OCMNonRetainingObjectReturnValueProvider alloc] initWithValue:anObject] autorelease];
    }
    else
    {
        action = [[[OCMObjectReturnValueProvider alloc] initWithValue:anObject] autorelease];
    }
    [[self stub] addInvocationAction:action];
    return self;
}

2.3 Invocation

NSLog(@”%@”, [classMock sayHi]); // print @”hello”

During the invocation, the classMock now will call its internal forwardInvocation class method. It checks the stub list whether contains the relevant invocation, and for the invocation whether there is a vaid action (value provider to be triggered).

3. References

TOC