iosobjective-cocmock

OCMock: invokeBlockWithArgs vs checkWithBlock


I'm reading the OCMock reference and I'm confused about these two OCMArg methods invokeBlockWithArgs (section 2.6)

The mock object will invoke the block passed as an argument to the stubbed method. If the block takes arguments and invokeBlock is used, the default values for the argument types are used, e.g. zero for a numerical type. Using invokeBlockWithArgs: it is possible to specify which arguments to invoke the block with; non-object arguments must be wrapped in value objects and the expression must be wrapped in round brackets.

and checkWithBlock (section 4.3)

For checkWithSelector:onObject:, when the mock object receives someMethod:, it invokes aSelector on anObject. If the method takes an argument the mock will pass the argument that was passed to someMethod:. The method should return a boolean indicating whether the argument matched the expectation or not.

So with checkWithBlock we can call the passed block with whatever arguments we provide, and with invokeBlockWithArgs it seems possible to do the same too. So when should I use the first or the second method?


Solution

  • checkWithBlock - you provide a block that will be called to assert that a value passed to the stubbed method, which you exchanged with [OCMArg checkWithBlock:] meets your expectations.

    invokeBlockWithArgs - this can be used when stubbing block arguments, to invoke them with sample arguments. This is needed if you want to check the behavior of the block.


    Let's imagine that we have a simple Networking client with just a single method :

    - (void)call:(NSURL *)url completion: (^void(NSData *, NSError *));
    

    We also have some ModelClass that takes an instance of our Networking as a dependency in init, and looks something like this :

    @property (nonatomic, nullable, strong) NSData *lastData;
    @property (nonatomic, nullable, strong) NSError *lastError;
    
    @property (nonatomic, strong) Networking *networking;
    
    - (instancetype)initWith:(Networking *)networking { /* */ }
    
    - (void)getData {
        [self.networking call:[NSURL URLWithString:@"www.stackoverflow.com"]
                   completion: ^(NSData *newData, NSError *newError) {
                                 self.lastData = newData;
                                 self.lastError = newError;
                   }];
    }
    
    

    We can then test the getData method like this in our test class :

    @property (nonatomic, strong) Networking *networkingMock;
    @property (nonatomic, strong) ModelClass *model;
    
    - (void)setUp {
        [super setUp];
    
        self.networkingMock = OCMClassMock([Networking class]);
        self.model = [[ModelClass alloc] initWith:self.networkingMock];
    }
    
    // Assert proper argument was passed by explicitly providing
    // expected value in `OCMStub`/`OCMExpect` call
    // OCMock will check that they are equal for us
    - (void)test_getData_passesCorrectURL {
    
        // Arrange
        OCMExpect([self.networkingMock call:[NSURL URLWithString:@"www.stackoverflow.com"] 
                                 completion:OCMOCK_ANY]);
    
        // Act
        [self.model getData];
    
        // Assert
        OCMVerifyAll(self.networkingMock);
    }
    
    // Assert proper argument is passed in a custom assertion block
    // OCMock will call this block, passing the value so that we can inspect it
    // We cannot use `invokeBlockWithArgs` to check the `url` parameter
    // because its not a block.
    - (void)test_getData_passesCorrectURL_withCheckWithBlock {
        // Arrange
        OCMExpect([self.networkingMock call:[OCMArg checkWithBlock:^BOOL(id value) {
                                 // This is the custom assertion block, we can inspect the `value` here
                                 // We need to return `YES`/`NO` depending if it matches our expectetations
    
                                 if (![value isKindOfClass:[NSURL class]]) { return NO };
                                 NSURL *valueAsURL = (NSURL *)value;
                                 return [valueAsURL isEqualToURL:[NSURL URLWithString:@"www.stackoverflow.com"]];
                                 }] 
                                 completion:OCMOCK_ANY]);
    
        // Act
        [self.model getData];
    
        // Assert
        OCMVerifyAll(self.networkingMock);
    }
    
    // We want to assert the behavior of the completion block passed to the `Networking`
    // in the `getData` method. So we need a way to invoke this block somehow - 
    // in previous two tests it was never called, because `OCMock` replaces the 
    // implementations of methods in stubbed classes.
    - (void)test_getData_shouldSetLastData_onCompletion {
    
        // Arrange
        NSData *expectedData = [NSData data];
        OCMExpect([self.networkingMock call:OCMOCK_ANY
                                 completion:[OCMArg invokeBlockWithArgs:expectedData, [NSNull null], nil]]);
    
        // Act
        [self.model getData];
    
        // Assert
        XCTAssertEqualObjects(self.model.lastData, expectedData);
    
    }
    
    

    In the last example, if you used checkWithBlock instead of invokeBlockWithArgs, the completion block passed in ModelClass wouldn't get called. Instead, the custom assertion block would be called (as we've seen in second test) and a pointer to the completion block would be passed as the value.

    You could of course cast this pointer to the block type, and invoke the block yourself with some arguments - but that's extra work that can be avoided thanks to invokeBlockWithArgs.

    Note : invokeBlockWithArgs takes a var_args list of arguments that needs to be terminated with nil to indicate the end. We need to use [NSNull null] to indicate that we want nil to be passed as a certain argument to our completion block - so in the example above our completion block will be called with expectedData as newData, and nil (from [NSNull null]) as newError.