我正在尝试为一个方法编写一个单元测试,该方法本身创建并将一个块传递给另一个对象(因此可以在以后调用)。此方法使用socket.io-objc向服务器发送请求。它将回调块传递给socketio的sendEvent:withData:andAcknowledge,当它从服务器收到响应时将调用它。这是我想测试的方法:
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();
}
}
这一切似乎在正常运行时工作正常。当我运行我的单元测试(使用SenTestingKit和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");
}
为了模拟响应,我需要捕获(并保留)由我正在测试的方法创建的块并传递给sendEvent。通过传递'andDo'参数到我的期望,我能够访问我正在寻找的块。但是,它似乎没有得到保留。因此,当sendEvent展开并且我去调用回调时,应该在块中捕获的所有值都显示为null。结果是,当我调用socketIoCallback时,测试崩溃,并且它访问最初作为块的一部分捕获的“回调”值(现在为零)。
我正在使用ARC,因此我希望“__block SocketIOCallback socketIoCallback”将保留值。我试图将块“-copy”到这个变量中,但它仍然没有保留在sendCommand的末尾。我该怎么做才能强制这个块保留足够长的时间来模拟响应?
注意:我已经尝试调用[invocation retainArguments],但是在测试完成后清理时会在objc_release中崩溃。
答案 0 :(得分:2)
我终于能够重现这个问题了,我怀疑错误出现在这段代码中:
SocketIOCallback temp;
[invocation getArgument:&temp atIndex:4];
以这种方式提取块无法正常工作。我不确定为什么,但它可能与一些神奇的ARC在后台做的有关。如果您将代码更改为以下代码,它应该按照您的预期工作:
void *pointer;
[invocation getArgument:&pointer atIndex:4];
SocketIOCallback temp = (__brigde SocketIOCallback)pointer;
答案 1 :(得分:1)
@aLevelOfIndirection是正确的,因为它是调用getArgument:atIndex:
。
要理解原因,请记住SocketIOCallback
是一个对象指针类型(它是块指针类型的typedef),它在ARC中隐式__strong
。因此&temp
是SocketIOCallback __strong *
类型,即它是指向强"的指针。当你将一个"指针传递给strong"对于一个函数,契约是如果函数替换指针指向的值(__strong
),它必须1)释放现有值,2)保留新值。
但是,NSInvocation
' getArgument:atIndex:
对所指向的事物的类型一无所知。它需要一个void *
参数,并简单地将所需的值按二进制方式复制到指针指向的位置。因此,简单来说,它会对temp
进行ARC之前的非保留分配。它不保留指定的值。
但是,由于temp
是一个强引用,ARC将假定它被保留,并在其范围的末尾释放它。这是一个过度释放,因此会崩溃。
理想情况下,编译器应该禁止在"指向强"之间的转换。和void *
,所以这不会意外发生。
解决方案是不将"指针传递给强"到getArgument:atIndex:
。您可以传递void *
作为aLevelOfIndirection show:
void *pointer;
[invocation getArgument:&pointer atIndex:4];
SocketIOCallback temp = (__brigde SocketIOCallback)pointer;
或"指向未上报的":
SocketIOCallback __unsafe_unretained pointer;
[invocation getArgument:&pointer atIndex:4];
SocketIOCallback temp = pointer;
然后再分配回强引用。 (或者在后一种情况下,如果你小心的话,可以直接使用未保留的参考文献)