10

Under ARC, I have an object, Child that has a weak property, parent. I'm trying to write some tests for Child, and I'm mocking its parent property using OCMock.

Under ARC, setting an NSProxy subclass using a synthesized weak property setter doesn't set the property ... the line after the weak property is set, checking it reveals that it's already nil. Here's the concrete example:

@interface Child : NSObject
@property (nonatomic, weak) id <ParentInterface>parent;
@end

@implementation Child
@synthesize parent = parent_;
@end

//  ... later, inside a test class ...

- (void)testParentExists
{
    // `mockForProtocol` returns an `NSProxy` subclass
    //
    OCMockObject *aParent = [OCMockObject mockForProtocol:@protocol(ParentInterface)];
    assertThat(aParent, notNilValue());

    // `Child` is the class under test
    //
    Child *child = [[Child alloc] init];
    assertThat(child, notNilValue());

    assertThat(child.parent, nilValue());
    child.parent = (id<ParentInterface>)aParent;
    assertThat([child parent], notNilValue());  // <-- This assertion fails
    [aParent self]; // <-- Added this reference just to ensure `aParent` was valid until the end of the test.
}

I know that I can get around this using an assign property instead of a weak property for the Child to reference the Parent, but then I'd have to nil out the parent when I was done with it (like some sort of caveman), which is exactly the sort of thing that ARC was supposed to obviate.

Any suggestions on how to make this test pass without changing my app code?

Edit: It seems to have to do with OCMockObject being an NSProxy, if I make aParent be an instance of NSObject, the child.parent weak reference "holds" a non-nil value. Still looking for a way to make this test pass without changing app code.

Edit 2: After accepting Blake's answer, I did an implementation in my project of a preprocessor macro that conditionally changed my properties from weak -> assign. Your mileage may vary:

#if __has_feature(objc_arc)
#define BBE_WEAK_PROPERTY(type, name) @property (weak, nonatomic) type name
#else
#define BBE_WEAK_PROPERTY(type, name) @property (assign, nonatomic) type name
#endif
prairiedogg
  • 6,323
  • 8
  • 44
  • 52
  • Check out this commit message: https://github.com/erikdoe/ocmock/commit/dbdb233ae84498077f7e946abb49731968333f0b Looks like the OCMock team is looking into the same thing. – Don Feb 03 '12 at 18:36

3 Answers3

9

We've been struggling with this same issue and it does indeed have to do with an incompatibility between ARC and weak references to NSProxy derived objects. I would recommend using a pre-processor directive to conditionally compile your weak delegate references to assign within the test suite so you can test them via OCMock.

Blake Watters
  • 6,607
  • 1
  • 43
  • 33
6

I found a different solution than a conditional macro since I was testing code that I could not change the code for.

I wrote a simple class that extends NSObject, not NSProxy, that forwards all selector invocations on to the OCMockProxy.

CCWeakMockProxy.h:

#import <Foundation/Foundation.h>

/**
 * This class is a hack around the fact that ARC weak references are immediately nil'd if the referent is an NSProxy
 * See: http://stackoverflow.com/questions/9104544/how-can-i-get-ocmock-under-arc-to-stop-nilling-an-nsproxy-subclass-set-using-a-w
 */
@interface CCWeakMockProxy : NSObject

@property (strong, nonatomic) id mock;

- (id)initWithMock:(id)mockObj;

+ (id)mockForClass:(Class)aClass;
+ (id)mockForProtocol:(Protocol *)aProtocol;
+ (id)niceMockForClass:(Class)aClass;
+ (id)niceMockForProtocol:(Protocol *)aProtocol;
+ (id)observerMock;
+ (id)partialMockForObject:(NSObject *)anObject;

@end

CCWeakMockProxy.m:

#import "CCWeakMockProxy.h"
#import <OCMock/OCMock.h>


#pragma mark Implementation
@implementation CCWeakMockProxy

#pragma mark Properties
@synthesize mock;

#pragma mark Memory Management
- (id)initWithMock:(id)mockObj {
    if (self = [super init]) {
        self.mock = mockObj;
    }
    return self;
}

#pragma mark NSObject
- (id)forwardingTargetForSelector:(SEL)aSelector {
    return self.mock;
}

- (BOOL)respondsToSelector:(SEL)aSelector {
    return [self.mock respondsToSelector:aSelector];
}

#pragma mark Public Methods
+ (id)mockForClass:(Class)aClass {
    return [[CCWeakMockProxy alloc] initWithMock:[OCMockObject mockForClass:aClass]];
}

+ (id)mockForProtocol:(Protocol *)aProtocol {
    return [[CCWeakMockProxy alloc] initWithMock:[OCMockObject mockForProtocol:aProtocol]];
}

+ (id)niceMockForClass:(Class)aClass {
    return [[CCWeakMockProxy alloc] initWithMock:[OCMockObject niceMockForClass:aClass]];
}

+ (id)niceMockForProtocol:(Protocol *)aProtocol {
    return [[CCWeakMockProxy alloc] initWithMock:[OCMockObject niceMockForProtocol:aProtocol]];
}

+ (id)observerMock {
    return [[CCWeakMockProxy alloc] initWithMock:[OCMockObject observerMock]];
}

+ (id)partialMockForObject:(NSObject *)anObject {
    return [[CCWeakMockProxy alloc] initWithMock:[OCMockObject partialMockForObject:anObject]];
}

@end

Just use the resulting object as you would a regular OCMockObject!

Greg Haines
  • 61
  • 1
  • 2
0

Sure. It's going nil because immediately after assigning child.parent, your proxy object itself is released by your test (since it's no longer referenced), and this causes the weak reference to nil out. So the solution is to keep your proxy object alive during the test. You can do this trivially by inserting a call to

[aParent self];

at the end of your method. That function call does nothing (-self just returns self), but it will ensure that ARC keeps the object alive.

An alternative would be to change your declaration of aParent to be __autoreleasing, which makes it behave more like MRR in that ARC will just leave an autoreleased reference in that slot instead of explicitly releasing the object when the variable goes out of scope. You can do that with

__autoreleasing OCMockObject *aParent = ...

That said, the first solution is probably cleaner, because you're explicitly keeping the object alive during the test.

Lily Ballard
  • 182,031
  • 33
  • 381
  • 347
  • I added the reference you suggested at the bottom of my test and have verified that it does not work. `parent` is still valid at the bottom of the test, but `child.parent` is always `nil`. – prairiedogg Feb 01 '12 at 23:19
  • @Prairiedogg: Perhaps your test is working as expected, then? Either that or OCMock isn't compatible with weak references (which may happen if it overrides retain/release). – Lily Ballard Feb 01 '12 at 23:26
  • Adding `__autoreleasing` also appears not to work. I can verify that the reference to `aParent` in the scope of the test is valid all the way to the last line of the test method, but the synthesized `child` property is never getting set - it's always `nil`. – prairiedogg Feb 01 '12 at 23:28
  • @Prairiedogg: Try changing it to `assign`. If that works, it implies that `OCMockObject` simply doesn't support weak references. If that also fails, then you definitely have a problem. Are you sure that `child` itself is non-nil? – Lily Ballard Feb 01 '12 at 23:37
  • Sure - I already verified that `assign` works (mentioned that in the original question). I did verify that `child` is not-nil. I also verified that changing `aParent` to an instance of `NSObject` makes `child.parent` "hold" correctly. – prairiedogg Feb 01 '12 at 23:41
  • @Prairiedogg: I think it's safe to say at this point that `OCMockObject` may not be compatible with weak references. Either that or something else is going on here that I can't think of. – Lily Ballard Feb 01 '12 at 23:52
  • I've tried all the steps without success. I also tried to turn off ARC for the test file with -fno-objc-arc without any success. So it leaves us with OCMock not being compatible with weak references, as said. – Pavel Kunc Apr 21 '12 at 17:30