2015-06-19 15 views
8

W moich testów jednostkowych, używam metody -[XCTestCase keyValueObservingExpectationForObject:keyPath:handler:] w celu zapewnienia, że ​​moja NSOperation kończy, tutaj jest code from my XCDYouTubeKit project:XCTest wyjątek podczas korzystania keyValueObservingExpectationForObject: keypath: obsługi:

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

Ten test zawsze przechodzi kiedy I uruchomić go lokalnie na moim Mac, ale czasami fails on Travis z tego błędu:

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

Czy robię coś źle?

+1

@ Cœur [Ogólny konsensus] (https://meta.stackoverflow.com/questions/274906/should-questions-that-violate-api-terms-of-service-be-flagged) jest taki, że nie jest odpowiedzialność stackoverflow użytkowników lub moderatorów w celu egzekwowania ToS innych stron internetowych. –

Odpowiedz

10

Twój kod jest poprawny, znalazłeś błąd w strukturze XCTest. Oto wyjaśnienie dogłębne, możesz pominąć koniec tej odpowiedzi, jeśli szukasz sposobu obejścia tego problemu.

Po wywołaniu keyValueObservingExpectationForObject:keyPath:handler: pod maską zostanie utworzony obiekt . Jest odpowiedzialny za obserwację obiektu/keyPath, który minąłeś. Po wywołaniu powiadomienia KVO, wywoływana jest metoda _safelyUnregister, w tym miejscu usuwany jest obserwator. Oto metoda (inżynierii wstecznej) metody _safelyUnregister.

@implementation _XCKVOExpectation 

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

@end 

Ta metoda jest wywoływana po raz kolejny pod koniec waitForExpectationsWithTimeout:handler: i gdy obiekt _XCKVOExpectation jest zwalniane. Zauważ, że operacja kończy się na wątku tła, ale test jest uruchamiany na głównym wątku. Masz więc warunek wyścigowy: jeśli _safelyUnregister jest wywoływany w głównym wątku, zanim właściwość ustawiona jest na YES na wątku tła, obserwator jest usuwany dwukrotnie, powodując, że nie może usunąć wyjątku obserwatora.

Aby obejść ten problem, należy zabezpieczyć metodę _safelyUnregister za pomocą blokady. Oto fragment kodu do kompilacji w celu testowym, który zajmie się naprawianiem tego błędu.

#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

Ten błąd został fixed in Xcode 7 beta 4.

Powiązane problemy