2015-06-19 55 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