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?
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
.