-
Notifications
You must be signed in to change notification settings - Fork 121
Crash when calling mocks from dealloc that are not called from elsewhere #141
Description
Hi Jon,
We're running into some issues where mocks injected into some code under test cause problems if the mock is called from the dealloc method AND ONLY the dealloc method.
Consider the following code under test:
@interface CodeUnderTest : NSObject <ObjectObserver>
@property (nonatomic, strong, readonly) ObservableObject *observableObject1;
@property (nonatomic, strong, readonly) ObservableObject *observableObject2;
@end
@implementation CodeUnderTest
- (instancetype)initWithObservableObject1:(ObservableObject *)observableObject1
observableObject2:(ObservableObject *)observableObject2
{
self = [super init];
if (self) {
_observableObject1 = observableObject1;
_observableObject2 = observableObject2;
[_observableObject1 addObserver:self];
}
return self;
}
- (void)doSomethingLater
{
[self.observableObject2 addObserver:self];
}
- (void)dealloc
{
[_observableObject1 removeObserver:self];
[_observableObject2 removeObserver:self];
}
@end
Note that in the init method, we observe the 1st observable, but not the 2nd. In dealloc, we stop observing both observables as we're not sure whether doSomethingLater will have been called at this point.
In a test, if I mock both observables and pass them in, the order in which I call stopMocking() suddenly becomes very important:
observableObject2thenobservableObject1works just fine, asdeallocwon't be called untilobservableObject1is stopped, so by the time we calldealloc, all mocks are stopped, and the calls toremoveObserver:on them do nothing.observableObject1thenobservableObject2causes a crash asdeallocis called as soon asobservableObject1is stopped and the call toremoveObserver:onobservableObject2then creates a strong reference from the mock to the object being disposed. When we stopobservableObject2, the crash manifests as we'll try to release an already released object.
Of course, this can be solved by making sure we stop the mocks in the correct order, but the test then has to be written with explicit knowledge of the implementation, and as the implementation changes, we may have to review the "stop order" to prevent further crashes. This doesn't strike me as a good thing!
I've created a PR (#140) which addresses the problem by separating the process of preventing the mock from receiving any more invocations (I call this "disabling" the mock in the PR), and stopping the mock. The downside is that tests now have to first disable, then stop all mocks to guarantee that this kind of crash won't happen.
Another option would be for OCMockito to ignore all invocations on a mock that come from the dealloc method.
WDYT?