15

I am trying to write an block of code using OCMock's stub andDo method.

In this case UIImageView extension class is being tested. I want to check that the extension calls [self setImage:] with parameter that is non-nil (later other image comparison will be used).

When using OCMock's andDo method, the test crashes with EXC_BAD_ACCESS after the block completes.

id mockView = [OCMockObject mockForClass:[UIImageView class]];
[[[mockView stub] andDo:^(NSInvocation *invocation)
  {
      UIImage *img;
      [invocation getArgument:&img atIndex:2]; <---- line causing the exception
      somebodySetImage |= (img != nil);

  }] setImage:OCMOCK_ANY];

  [mockView do_something_that_calls_setImage];

The only solution that I've found for now is using andCall instead of andDo, but this complicates the test.

Can I avoid the crash with andDo?

UPDATE Well, I will try to give a better example here: Here is the new piece of the test code:

- (void)testDownloadingThumbnail
{
    PInfo *_sut = [[PInfo alloc] init];

    __block id target = nil;

    id mock = [OCMockObject mockForClass:[NSOperationQueue class]];

    [[[mock expect] andDo:^(NSInvocation *inv)
    {
        NSInvocationOperation *op;
        [inv getArgument:&op atIndex:2];
        target = [[op invocation] target]; /* replacing this line with STAssert does not help either */
    }] addOperation:OCMOCK_ANY];

    [_sut setDownloadQueue:mock];
    [_sut startDownloadingImagesAsync:YES];

    [mock verify];

    STAssertEqualObjects(target, _sut, @"invalid op target");
}

Here is the tested code (single method from PInfo):

- (void)startDownloadingImagesAsync:(bool)isThumbnailImg
{
    NSInvocationOperation *inv;

    inv = [[NSInvocationOperation alloc] initWithTarget:self
                                     selector:@selector(loadThumbnailWorker:)
                                       object:nil];
    [[self downloadQueue] addOperation:inv];
}

The code still crashes upon exit from startDownloadingImagesAsync with EXC_BAD_ACCESS. If I add a breakpoint inside the andDo block, I see that the control reaches this point and retrieves correct objects via getArgument.

Yet, if I use getArgument inside the block, it crashes whatever I try to do.

P.S. Thanks for help.

UrK
  • 2,191
  • 2
  • 26
  • 42
  • Can you elaborate on what you mean by `[mockView do_something_that_calls_setImage];`? – Christopher Pickslay Nov 08 '12 at 06:58
  • Something along the following lines: -(void)do_something_that_calls_setImage { [self setImage:[UIImage imagedNamed:@"f.png"]; } – UrK Nov 08 '12 at 07:42
  • I think you need to post a bit more code. You shouldn't be invoking a method directly on `mockView`--it's just a proxy for mocking. I have to assume that your last line is a call to some other object that has a reference to `mockView`. If you can clarify, or explain what you're trying to achieve in the test, I can probably help. – Christopher Pickslay Nov 13 '12 at 20:52
  • Hi, @ChristopherPickslay. I missed the notification somehow. In this case I am trying to test the other method of _mockView_: _do_something_that_calls_setImage_. That method uses _mockView_'s own _setImage_ method. But this fails in the case you specified too: calling _ do_something_that_calls_setImage_ on an object that holds reference to mockView. I am unable to work with parameters of _andDo_. Trying to do this always causes _EXC_BAD_ACCESS_. – UrK Nov 25 '12 at 15:47
  • Just curious, are you are using ARC? – Ben Flynn Dec 04 '12 at 02:11
  • I was having this same issue. I tried using the NSInvocationOCMAdditions.h and it magically went away, to the point where I'm even more confused about the behavior than before. Anyway, [invocation getArgumentAtIndexAsObject:3] was working for me in the andDo: if you're still seeing this. https://github.com/erikdoe/ocmock/blob/master/Source/OCMock/NSInvocation%2BOCMAdditions.h – Ben Flynn Dec 04 '12 at 02:31
  • @BenFlynn, I tried using the file you suggested and, regretfully, it did not help. Still crashes with EXC_BAD_ACCESS. And yes; I am using ARC. – UrK Dec 11 '12 at 14:11

3 Answers3

33

I ran into a similar problem when using NSProxy's forwardInvocation: method.

Can you try the below?

NSInvocationOperation *op; // Change this line
__unsafe_unretained NSInvocationOperation *op; // to this line

Or another approach could be to retain NSInvocation's arguments:

[invocation retainArguments];

http://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSInvocation_Class/Reference/Reference.html#//apple_ref/occ/instm/NSInvocation/retainArguments

I'll try to add a more detailed explanation later.

pshah
  • 2,052
  • 1
  • 21
  • 40
  • The strange part here is that I tried to move the variable outside of the block with "__block NSInvocationOperation *op;" and then try to use inside the block. And yet, the test crashed, though it should not have been released by ARC. I will be really glad to know the explanation. And... Thanks a lot! – UrK Dec 12 '12 at 10:10
  • 7
    Without the __unsafe_unretained, an object gets assigned to that address by the getArgument: method, but ARC has no clue since that takes a "void \*" argument (not "id \*"). Therefore, ARC does not know to add a -retain when that happens. However, as a strongly-typed local variable, it *does* add a release when it goes out of scope. Thus, the object will get over-released. The easiest way is to just have the local variable be __unsafe_unretained so ARC leaves it alone. – Carl Lindberg Mar 17 '14 at 21:42
  • Thank you! Using __unsafe_unretained for local variables assigned by getArgument:atIndex: fixed our crashes. Note that [invocation retainArguments] didn't help. – roustem Apr 09 '14 at 20:25
  • [invocation retainArguments]; was what fixed this for me! – Lee Dec 09 '15 at 16:08
0

I think the problem is that you're trying to invoke a mock object directly. For what you're trying to do, you shouldn't need a mock object. Just call the method and verify that the image was set:

expect(myObject.imageView.image).to.beNil();
[myObject do_something_that_calls_setImage];
expect(myObject.imageView.image).not.to.beNil();

If you really want to use a mock for some reason, you could do it with a real UIImageView and a partial mock:

UIImageView *imageView = myObject.imageView;
id mockView = [OCMockObject partialMockForObject:imageView];
__block BOOL imageSet = NO;
[[[mockView stub] andDo:^(NSInvocation *invocation) {
      UIImage *img;
      [invocation getArgument:&img atIndex:2];
      imageSet = (img != nil);
  }] setImage:OCMOCK_ANY];

[myObject do_something_that_calls_setImage];
expect(imageSet).to.beTruthy();
Christopher Pickslay
  • 17,523
  • 6
  • 79
  • 92
  • I've updated the original question with new piece of real code. I do understand that I do not need the mock in this case, but just for the sake of this example... There are many cases, in which I have no choice but to use mocks. – UrK Nov 26 '12 at 09:15
0

In my case this was happening because I introduced another parameter to this method, so the block parameter got shifted by one.

I fixed it by changing [inv getArgument:&op atIndex:2] to [inv getArgument:&op atIndex:3]

rounak
  • 9,217
  • 3
  • 42
  • 59