2015-06-19 3 views
8

В моих модульных тестов, я использую метод -[XCTestCase keyValueObservingExpectationForObject:keyPath:handler:] для того, чтобы гарантировать, что мой NSOperation заканчивается, вот code from my XCDYouTubeKit project:XCTest исключение при использовании keyValueObservingExpectationForObject: Ключевой путь: обработчик:

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

EDIT

Эта ошибка была fixed in Xcode 7 beta 4.