ios – 使用keyValueObservingExpectationForObject时的XCTest异常: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对象.它负责观察您传递的对象/ 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对象时.请注意,操作在后台线程上终止,但测试在主线程上运行.所以你有一个竞争条件:如果在后台线程上将hasUnregistered属性设置为YES之前在主线程上调用了_safelyUnregister,则会删除观察者两次,导致无法删除观察者异常.

因此,为了解决此问题,您必须使用锁保护_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);
        }
    }));
}

编辑

这个bug已经是fixed in Xcode 7 beta 4了.

相关文章

当我们远离最新的 iOS 16 更新版本时,我们听到了困扰 Apple...
欧版/美版 特别说一下,美版选错了 可能会永久丧失4G,不过只...
一般在接外包的时候, 通常第三方需要安装你的app进行测...
前言为了让更多的人永远记住12月13日,各大厂都在这一天将应...