2015-06-19 28 views
8

在我的单元测试,我为了现在用的-[XCTestCase keyValueObservingExpectationForObject:keyPath:handler:]方法来确保我的NSOperation完成,这里是code from my XCDYouTubeKit projectXCTest异常时keyValueObservingExpectationForObject:的keyPath:处理器:

- (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."

难道我做错了什么?

+1

@Cœur[一般共识](https://meta.stackoverflow.com/questions/274906/should-questions-that-violate-api-terms-of-service-be-flagged)是它不是stackoverflow用户或版主执行其他网站的ToS的责任。 –

回答

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 

此方法是在waitForExpectationsWithTimeout:handler:结束再次调用,并且当_XCKVOExpectation对象被释放。请注意,该操作在后台线程上终止,但测试在主线程上运行。所以你有一个竞争条件:如果_safelyUnregister被调用主线程之前属性设置为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); 
     } 
    })); 
} 

编辑

这个bug已经fixed in Xcode 7 beta 4