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

0xc*_*ced 8 macos key-value-observing ios xctest

在我的单元测试中,我使用的-[XCTestCase keyValueObservingExpectationForObject:keyPath:handler:]方法是为了确保我的NSOperation完成,这是我的XCDYouTubeKit项目代码:

- (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];
}
Run Code Online (Sandbox Code Playgroud)

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

失败:捕获"NSRangeException","无法从<XCDYouTubeVideoOperation 0x1001b9510>中删除关键路径"isFinished"的观察者<_XCKVOExpectation 0x1001846c0>,因为它未注册为观察者."

难道我做错了什么?

0xc*_*ced 10

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

当你打电话时keyValueObservingExpectationForObject:keyPath:handler:,_XCKVOExpectation会在引擎盖下创建一个对象.它负责观察您传递的对象/ keyPath.一旦KVO通知被触发,_safelyUnregister就会调用该方法,这是删除观察者的地方.这是该方法的(逆向工程)实现_safelyUnregister.

@implementation _XCKVOExpectation

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

@end
Run Code Online (Sandbox Code Playgroud)

此方法在结束时再次调用waitForExpectationsWithTimeout:handler:以及当所述_XCKVOExpectation对象被释放.请注意,操作在后台线程上终止,但测试在主线程上运行.所以你有一个竞争条件:如果_safelyUnregisterhasUnregistered属性设置为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);
        }
    }));
}
Run Code Online (Sandbox Code Playgroud)

编辑

此错误已在Xcode 7 beta 4中修复.