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
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.
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With