使用 keyValueObservingExpectationForObject:keyPath:handler 时的 XCTest 异常:
XCTest exception when using keyValueObservingExpectationForObject:keyPath:handler:
在我的单元测试中,我使用 -[XCTestCase keyValueObservingExpectationForObject:keyPath:handler:]
方法来确保我的 NSOperation 完成,这里是 code from my XCDYouTubeKit project:
- (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."
我是不是做错了什么?
您的代码是正确的,您在 XCTest 框架中发现了一个错误。这是一个深入的解释,如果您只是在寻找解决方法,可以跳到这个答案的末尾。
当您调用 keyValueObservingExpectationForObject:keyPath:handler:
时,会在后台创建一个 _XCKVOExpectation
对象。它负责观察你传递的object/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
在后台线程上 hasUnregistered
属性 设置为 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);
}
}));
}
编辑
此错误已 fixed in Xcode 7 beta 4。
在我的单元测试中,我使用 -[XCTestCase keyValueObservingExpectationForObject:keyPath:handler:]
方法来确保我的 NSOperation 完成,这里是 code from my XCDYouTubeKit project:
- (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."
我是不是做错了什么?
您的代码是正确的,您在 XCTest 框架中发现了一个错误。这是一个深入的解释,如果您只是在寻找解决方法,可以跳到这个答案的末尾。
当您调用 keyValueObservingExpectationForObject:keyPath:handler:
时,会在后台创建一个 _XCKVOExpectation
对象。它负责观察你传递的object/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
在后台线程上 hasUnregistered
属性 设置为 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);
}
}));
}
编辑
此错误已 fixed in Xcode 7 beta 4。