Skip to content

Crash when calling mocks from dealloc that are not called from elsewhere #141

@cerihughes

Description

@cerihughes

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:

  • observableObject2 then observableObject1 works just fine, as dealloc won't be called until observableObject1 is stopped, so by the time we call dealloc, all mocks are stopped, and the calls to removeObserver: on them do nothing.
  • observableObject1 then observableObject2 causes a crash as dealloc is called as soon as observableObject1 is stopped and the call to removeObserver: on observableObject2 then creates a strong reference from the mock to the object being disposed. When we stop observableObject2, 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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions