2015-06-19 13 views
8

In meinen Unit-Tests, ich bin mit der -[XCTestCase keyValueObservingExpectationForObject:keyPath:handler:] Methode, um sicherzustellen, dass meine NSOperation abgeschlossen ist, ist hier die code from my XCDYouTubeKit project:XCTest Ausnahme bei der Verwendung von keyValueObservingExpectationForObject: keyPath: handler:

- (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]; 
} 

Dieser Test immer passiert, wenn ich betreibe es lokal auf meinem Mac, aber manchmal ist es fails on Travis mit diesem Fehler:

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

Bin ich etwas falsch?

+1

@ Cœur [Der allgemeine Konsens] (https://meta.stackoverflow.com/questions/274906/should-questions-that-violate-api-terms-of-service-be-flagged) ist, dass es nicht ist die Verantwortung von stackoverflow-Benutzern oder -Moderatoren, die ToS anderer Websites durchzusetzen. –

Antwort

10

Ihr Code ist korrekt, Sie haben einen Fehler im XCTest-Framework gefunden. Hier ist eine ausführliche Erklärung, Sie können zum Ende dieser Antwort springen, wenn Sie nur nach einer Problemumgehung suchen. Wenn Sie keyValueObservingExpectationForObject:keyPath:handler: aufrufen, wird unter der Haube ein _XCKVOExpectation Objekt erstellt. Es ist dafür verantwortlich, das von Ihnen übergebene Objekt/keyPath zu beobachten. Sobald die KVO-Benachrichtigung ausgelöst wurde, wird die _safelyUnregister-Methode aufgerufen. Hier wird der Beobachter entfernt. Hier ist die (reverse engineered) Implementierung der _safelyUnregister Methode.

@implementation _XCKVOExpectation 

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

@end 

Diese Methode wird am Ende der waitForExpectationsWithTimeout:handler: und wenn das _XCKVOExpectation Objekt freigegeben erneut aufgerufen. Beachten Sie, dass die Operation in einem Hintergrundthread endet, der Test jedoch im Hauptthread ausgeführt wird. Sie haben also eine Race-Bedingung: Wenn _safelyUnregister im Haupt-Thread aufgerufen wird, bevor die Eigenschaft auf YES auf dem Hintergrund-Thread festgelegt ist, wird der Beobachter zweimal entfernt, wodurch die kann keine Beobachter Ausnahme entfernen. Um dieses Problem zu umgehen, müssen Sie die _safelyUnregister Methode mit einer Sperre schützen. Hier ist ein Code-Snippet, mit dem Sie Ihr Testziel kompilieren können, das sich um die Behebung dieses Fehlers kümmert.

#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); 
     } 
    })); 
} 

EDIT

Dieser Fehler ist fixed in Xcode 7 beta 4 gewesen.