使用 keyValueObservingExpectationForObject:keyPath:handler 时的 XCTest 异常:

XCTest exception when using keyValueObservingExpectationForObject:keyPath:handler:

在我的单元测试中,我使用 -[XCTestCase keyValueObservingExpectationForObject:keyPath:handler:] 方法来确保我的 NSOperation 完成,这里是 code from my XCDYouTubeKit project:

- (void) testStartingOnBackgroundThread
{
    XCDYouTubeVideoOperation *operation = [[XCDYouTubeVideoOperation alloc] initWithVideoIdentifier:nil languageIdentifier:nil];
    [self keyValueObservingExpectationForObject:operation keyPath:@"isFinished" handler:^BOOL(id observedObject, NSDictionary *change)
    {
        XCTAssertNil([observedObject video]);
        XCTAssertNotNil([observedObject error]);
        return YES;
    }];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        XCTAssertFalse([NSThread isMainThread]);
        [operation start];
    });
    [self waitForExpectationsWithTimeout:5 handler:nil];
}

当我在 Mac 本地 运行 时,此测试总是通过,但有时它 fails on Travis 并出现此错误:

failed: caught "NSRangeException", "Cannot remove an observer <_XCKVOExpectation 0x1001846c0> for the key path "isFinished" from <XCDYouTubeVideoOperation 0x1001b9510> because it is not registered as an observer."

我是不是做错了什么?

您的代码是正确的,您在 XCTest 框架中发现了一个错误。这是一个深入的解释,如果您只是在寻找解决方法,可以跳到这个答案的末尾。

当您调用 keyValueObservingExpectationForObject:keyPath:handler: 时,会在后台创建一个 _XCKVOExpectation 对象。它负责观察你传递的object/keyPath。一旦触发了 KVO 通知,就会调用 _safelyUnregister 方法,这是移除观察者的地方。这是 _safelyUnregister 方法的(逆向工程)实现。

@implementation _XCKVOExpectation

- (void) _safelyUnregister
{
    if (!self.hasUnregistered)
    {
        [self.observedObject removeObserver:self forKeyPath:self.keyPath];
        self.hasUnregistered = YES;
    }
}

@end

此方法在 waitForExpectationsWithTimeout:handler: 结束时和 _XCKVOExpectation 对象被释放时再次调用。请注意,该操作在后台线程上终止,但测试是 运行 在主线程上。所以你有一个竞争条件:如果 _safelyUnregister 在后台线程上 hasUnregistered 属性 设置为 YES 之前在主线程上调用,则观察者被删除两次,导致 无法删除观察者 异常。

因此,为了解决此问题,您必须使用锁来保护 _safelyUnregister 方法。这是一个代码片段,供您在测试目标中编译,它将修复此错误。

#import <objc/runtime.h>

__attribute__((constructor)) void WorkaroundXCKVOExpectationUnregistrationRaceCondition(void);
__attribute__((constructor)) void WorkaroundXCKVOExpectationUnregistrationRaceCondition(void)
{
    SEL _safelyUnregisterSEL = sel_getUid("_safelyUnregister");
    Method safelyUnregister = class_getInstanceMethod(objc_lookUpClass("_XCKVOExpectation"), _safelyUnregisterSEL);
    void (*_safelyUnregisterIMP)(id, SEL) = (__typeof__(_safelyUnregisterIMP))method_getImplementation(safelyUnregister);
    method_setImplementation(safelyUnregister, imp_implementationWithBlock(^(id self) {
        @synchronized(self)
        {
            _safelyUnregisterIMP(self, _safelyUnregisterSEL);
        }
    }));
}

编辑

此错误已 fixed in Xcode 7 beta 4