1

我正在尝试为一个方法编写一个单元测试,该方法本身创建一个块并将其传递给另一个对象(以便以后可以调用它)。此方法使用 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”会保留值。我试图将块“复制”到这个变量中,但它似乎仍然没有保留到 sendCommand 的末尾。我能做些什么来强制这个块保留足够长的时间让我模拟响应?

注意:我试过调用 [invocation retainArguments] ,它可以工作,但在测试完成后清理时会在 objc_release 的某个地方崩溃。

4

2 回答 2

2

我终于能够重现该问题,并且我怀疑此代码部分中存在错误:

SocketIOCallback temp;
[invocation getArgument:&temp atIndex:4];

以这种方式提取块无法正常工作。我不确定为什么,但这可能与 ARC 在后台所做的一些魔术有关。如果您将代码更改为以下内容,它应该可以按预期工作:

void *pointer;
[invocation getArgument:&pointer atIndex:4];
SocketIOCallback temp = (__brigde SocketIOCallback)pointer;
于 2013-05-02T21:43:48.693 回答
1

@aLevelOfIndirection 是正确的,因为它是由于调用getArgument:atIndex:.

要理解原因,请记住这SocketIOCallback是一个对象指针类型(它是块指针类型的 typedef),它隐含__strong在 ARC 中。&temptype也是如此SocketIOCallback __strong *,即它是“指向强的指针”。当您将“指向强的指针”传递给函数时,约定是如果函数替换指针指向的值(即__strong),它必须 1)释放现有值,并且 2)保留新值。

但是,NSInvocation'sgetArgument:atIndex:对所指向的事物的类型一无所知。它接受一个void *参数,并简单地将所需的值以二进制方式复制到指针指向的位置。因此,简单来说,它对temp. 它不保留分配的值。

然而,由于temp是一个强引用,ARC 将假定它被保留,并在其范围结束时释放它。这是一个过度释放,因此会崩溃。

理想情况下,编译器应该禁止“强指针”和 之间的转换void *,所以这不会意外发生。

解决方案是不要将“指向强的指针”传递给getArgument:atIndex:. 您可以通过void *aLevelOfIndirection 显示的任何一个:

void *pointer;
[invocation getArgument:&pointer atIndex:4];
SocketIOCallback temp = (__brigde SocketIOCallback)pointer;

或“指向未保留的指针”:

SocketIOCallback __unsafe_unretained pointer;
[invocation getArgument:&pointer atIndex:4];
SocketIOCallback temp = pointer;

然后再分配回一个强参考。(或者在后一种情况下,如果你小心的话,你可以直接使用未保留的引用)

于 2013-05-03T23:00:03.340 回答