I am trying to write a unit test for a method which itself creates and passes a block to another object (so it can be called later). This method is using socket.io-objc to send a request to a server. It passes a callback block to socketio's sendEvent:withData:andAcknowledge that will be invoked when it receives a response from the server). Here is the method i want to test:
typedef void(^MyResponseCallback)(NSDictionary * response);
...
-(void) sendCommand:(NSDictionary*)dict withCallback:(MyResponseCallback)callback andTimeoutAfter:(NSTimeInterval)delay
{
__block BOOL didTimeout = FALSE;
void (^timeoutBlock)() = nil;
// create the block to invoke if request times out
if (delay > 0)
{
timeoutBlock = ^
{
// set the flag so if we do get a response we can suppress it
didTimeout = TRUE;
// invoke original callback with no response argument (to indicate timeout)
callback(nil);
};
}
// create a callback/ack to be invoked when we get a response
SocketIOCallback cb = ^(id argsData)
{
// if the callback was invoked after the UI timed out, ignore the response. otherwise, invoke
// original callback
if (!didTimeout)
{
if (timeoutBlock != nil)
{
// cancel the timeout timer
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(onRequestTimeout:) object:timeoutBlock];
}
// invoke original callback
NSDictionary * responseDict = argsData;
callback(responseDict);
}
};
// send the event to the server
[_socketIO sendEvent:@"message" withData:dict andAcknowledge:cb];
if (timeoutBlock != nil)
{
// if a timeout value was specified, set up timeout now
[self performSelector:@selector(onRequestTimeout:) withObject:timeoutBlock afterDelay:delay];
}
}
-(void) onRequestTimeout:(id)arg
{
if (nil != arg)
{
// arg is a block, execute it
void (^callback)() = (void (^)())arg;
callback();
}
}
This all appears to be working fine when running for real. My problem comes in when I run my unit test (which uses SenTestingKit and OCMock):
-(void)testSendRequestWithNoTimeout
{
NSDictionary * cmd = [[NSDictionary alloc] initWithObjectsAndKeys:
@"TheCommand", @"msg", nil];
__block BOOL callbackInvoked = FALSE;
__block SocketIOCallback socketIoCallback = nil;
MyResponseCallback requestCallback = ^(NSDictionary * response)
{
STAssertNotNil(response, @"Response dictionary is invalid");
callbackInvoked = TRUE;
};
// expect controller to emit the message
[[[_mockSocket expect] andDo:^(NSInvocation * invocation) {
SocketIOCallback temp;
[invocation getArgument:&temp atIndex:4];
// THIS ISN'T WORKING AS I'D EXPECT
socketIoCallback = [temp copy];
STAssertNotNil(socketIoCallback, @"No callback was passed to socket.io sendEvent method");
}] sendEvent:@"message" withData:cmd andAcknowledge:OCMOCK_ANY];
// send command to dio
[_ioController sendCommand:cmd withCallback:requestCallback];
// make sure callback not invoked yet
STAssertFalse(callbackInvoked, @"Response callback was invoked before receiving a response");
// fake a response coming back
socketIoCallback([[NSDictionary alloc] initWithObjectsAndKeys:@"msg", @"response", nil]);
// make sure the original callback was invoked as a result
STAssertTrue(callbackInvoked, @"Original requester did not get callback when msg recvd");
}
To simulate a response, I need to capture (and retain) the block that is created by the method i'm testing and is passed to sendEvent. By passing an 'andDo' argument to my expectation, I am able to access the block i'm looking for. However, it seems like it is not being retained. So, when sendEvent unwinds and I go to invoke the callback, all the values that should have been captured in the block show up as null. The result is that the test crashes when I invoke socketIoCallback and it goes to access the 'callback' value that was originally captured as part of the block (and is now nil).
I am using ARC and so I expect that "__block SocketIOCallback socketIoCallback" will retain values. I've tried to "-copy" the block into this variable but still it does not seem to retain past the end of sendCommand. What can I do to force this block to retain long enough for me to simulate a response?
Note: I've tried calling [invocation retainArguments] which works but then crashes somewhere in objc_release when cleaning up after the test is complete.