13

I have a pretty simple setup for this unit test. I have a class that has a delegate property:

@interface MyClass : NSObject
...
@property (nonatomic, weak) id<MyDelegateProtocol> connectionDelegate;
...
@end

and I set the delegate in my test:

- (void)testMyMethod_WithDelegate {
  id delegate = mockDelegateHelper(); // uses OCMock to create a mock object
  [[delegate expect] someMethod];
  myClassIvar.connectionDelegate = delegate;
  [myClass someOtherMethod];
  STAssertNoThrow([delegate verify], @"should have called someMethod on delegate.");
}

But the delegate is not actually set on line 3 of my unit test, so #someMethod is never called. When I change it to

myClassIvar.connectionDelegate = delegate;
STAssertNotNil(myClassIvar.connectionDelegate, @"delegate should not be nil");

it fails there. I'm using ARC, so my hunch was that the weak property was being deallocated. Sure enough, changing it to strong makes the STAssertNotNil pass. But I don't want to do that with a delegate, and I don't understand why that makes a difference here. From what I've read, all local references in ARC are strong, and STAssertNotNil(delegate) passes. Why is my weak delegate property nil when the same object in a local variable is not?

Adam Stegman
  • 1,013
  • 10
  • 14

4 Answers4

8

This is a bug in the iOS runtime. The following discussion has more detail. In a nutshell, the iOS ARC runtime can't seem to handle weak references to proxies. The OSX runtime can.

http://www.mulle-kybernetik.com/forum/viewtopic.php?f=4&t=252

As far as I understand from the discussion a bug report has been filed with Apple. If anyone has a sensible idea for a workaround...

Erik Doernenburg
  • 2,933
  • 18
  • 21
  • +1 Great, it seems I had [the right idea](http://stackoverflow.com/a/9058542/31158).... Thanks for finding the right information! – Jordão Mar 23 '12 at 21:13
  • You might want to try the iOS 6.0 Simulator. If the weak/Proxy problem is a runtime bug (and it is), this might be solved in iOS 6.0. I tested it, but can't comment on it (since it is still under NDA). But you really should try it. Really. – Jelle Jun 27 '12 at 12:19
4

I don't really know what's happening here, but OCMock returns an autoreleased NSProxy-descendant from the mockForProtocol: method, which I think is right. Maybe ARC has problems with NSProxies? Anyway, I've overcome this problem by declaring the variable __weak:

- (void)testMyMethod_WithDelegate {
  // maybe you'll also need this modifier inside the helper
  __weak id delegate = mockDelegateHelper(); 
  ...

It really doesn't need to be __strong (the default) in this case, as it's autoreleased and you're not keeping it around...

Jordão
  • 55,340
  • 13
  • 112
  • 144
  • any hint or explanation you would like to share on this? – defvol Jun 30 '12 at 23:10
  • 1
    @rodbot: well, I just thought that ARC had a problem with `NSProxy`s because that's what OCMock uses, and by declaring a variable with `__weak` you're basically telling ARC not to bother with it. – Jordão Jul 01 '12 at 00:02
2

A workaround is to use Partial Mocks.

@interface TestMyDelegateProtocolDelegate : NSObject <MyDelegateProtocol>
@end

@implementation TestMyDelegateProtocolDelegate
- (void)someMethod {}
@end


@implementation SomeTest {
- (void)testMyMethod_WithDelegate {
  id<MyDelegateProtocol> delegate = [[TestMyDelegateProtocolDelegate] alloc] init];
  id delegateMock = [OCMockObject partialMockForObject:delegate]
  [[[delegateMock expect] someMethod]
  myClassIvar.connectionDelegate = delegate;
  [myClass someOtherMethod];
  STAssertNoThrow([delegate verify], @"should have called someMethod on delegate.");
}
@end
fabb
  • 11,660
  • 13
  • 67
  • 111
  • But isn't perfect. It require to include dependencies from other Classes than the Class you are testing – Luca Bartoletti Aug 21 '12 at 16:00
  • 1
    Don't I also have to include dependencies if I do it the regular painless way `[OCMockObject mockForProtocol:@protocol(MyDelegateProtocol)]`? – fabb Aug 22 '12 at 13:21
1

I am no ARC expert but my guess is that mockDelegateHelper() is returning a weak object. As a result delegate is nil before the second line of code executes. I would venture to guess that either the mockDelegateHelper() is the culprit or that OCMock is getting in the way with how it manipulates and creates objects.

Evan
  • 6,151
  • 1
  • 26
  • 43
  • Removing the function from the flow didn't affect it, but removing OCMock fixed the issue. I made an explicit class that conforms to my protocol and used it instead, and the delegate is no longer nil. This is unfortunate for my testing, but at least it answers my question. Thank you. – Adam Stegman Dec 31 '11 at 04:45