Your code is correct; you have encountered a bug in the XCTest framework. Here is a detailed explanation, you can skip to the end of this answer if you are just looking for a workaround.
When you call keyValueObservingExpectationForObject:keyPath:handler: _XCKVOExpectation object is created under the hood. It is responsible for monitoring the / keyPath object that you passed. Once the KVO notification is triggered, the _safelyUnregister method is _safelyUnregister , in which the observer is deleted. Here is a (reverse engineering) implementation of the _safelyUnregister method.
@implementation _XCKVOExpectation - (void) _safelyUnregister { if (!self.hasUnregistered) { [self.observedObject removeObserver:self forKeyPath:self.keyPath]; self.hasUnregistered = YES; } } @end
This method is called again at the end of waitForExpectationsWithTimeout:handler: and when the _XCKVOExpectation object _XCKVOExpectation freed. Note that the operation ends with a background thread, but the test runs in the main thread. So you have a race condition: if _safelyUnregister is called in the main thread before the hasUnregistered property hasUnregistered set to YES in the background thread, the observer is deleted twice, as a result of which Can cannot remove the observer exception.
So, to get around this problem, you must protect the _safelyUnregister method with a lock. Here is a snippet of code to compile for a test purpose that will take care of fixing this error.
#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
This bug has been fixed in Xcode 7 beta 4 .
0xced
source share