3

I'm having an issue with the OCMock framework for iOS. I'm essentially trying to mock UIAlertView's initWithTitle:message:delegate... method. The example below doesn't work in the sense that the stubbed return value isn't returned when I call the initWithTitle method.

UIAlertView *emptyAlert = [UIAlertView new];
id mockAlert = [OCMockObject partialMockForObject:[UIAlertView alloc]];
[[[mockAlert stub] andReturn:emptyAlert] initWithTitle:OCMOCK_ANY message:OCMOCK_ANY delegate:nil cancelButtonTitle:OCMOCK_ANY otherButtonTitles:nil];

UIAlertView *testAlertReturnValue = [[UIAlertView alloc] initWithTitle:@"title" message:@"message" delegate:nil cancelButtonTitle:@"ok" otherButtonTitles:nil];
if(testAlertReturnValue == emptyAlert) {
    NSLog(@"UIAlertView test worked");
}

However, it does work if I use the same idea for NSDictionary.

NSDictionary *emptyDictionary = [NSDictionary new];
id mockDictionary = [OCMockObject partialMockForObject:[NSDictionary alloc]];
[[[mockDictionary stub] andReturn:emptyDictionary] initWithContentsOfFile:OCMOCK_ANY];

NSDictionary *testDictionaryReturnValue = [[NSDictionary alloc] initWithContentsOfFile:@"test"];
if(testDictionaryReturnValue == emptyDictionary) {
    NSLog(@"NSDictionary test worked");
}

One thing I notice is that the method "forwardInvocationForRealObject:" in "OCPartialMockObject.m" is called during the NSDictionary initWithContentsOfFile call, but not during the UIAlertView initWithTitle call.

Could this be an OCMock bug?

Makoto
  • 104,088
  • 27
  • 192
  • 230
larromba
  • 306
  • 2
  • 11
  • Why would you do this? If you want to prevent/intercept the UIAlertView in your test, it would be cleaner to put the alert view in a method like `showAlert` in your class, and do a partial mock of that method. – Christopher Pickslay Nov 18 '13 at 19:48
  • The UIAlertView method was in a class method in a category, so I wanted to keep it in the method, but you're making a valid point here. The thing is, if you want to make sure the method showAlert actually calls an the [alert show] method, you're faced with the same problem – larromba Nov 20 '13 at 01:41

3 Answers3

9

Here's a more recent example, OCMock now supports class mocks.

id mockAlertView = [OCMockObject mockForClass:[UIAlertView class]];
[[[mockAlertView stub] andReturn:mockAlertView] alloc];
(void)[[[mockAlertView expect] andReturn:mockAlertView]
    initWithTitle:@"Title"
          message:@"Message"
         delegate:OCMOCK_ANY
cancelButtonTitle:OCMOCK_ANY
otherButtonTitles:OCMOCK_ANY, nil];
[[mockAlertView expect] show];

// code that will display the alert here

[mockAlertView verify];
[mockAlertView stopMocking];

It's pretty common to have the alert being triggered from a callback on something. One way to wait for that is using a verifyWithDelay, see https://github.com/erikdoe/ocmock/pull/59.

Olcay Ertaş
  • 5,987
  • 8
  • 76
  • 112
dB.
  • 4,700
  • 2
  • 46
  • 51
  • This is by far the cleanest and simplest solution to mocking a UIAlertView class in a test. One thing to add is that if you set tags for your alert views to distinguish them, you will want to 'expect' that the tag is set in your test as well: `[[mockAlertView expect] setTag:MY_TAG]` – manderson Mar 10 '14 at 17:54
  • 1
    I've been using this in a project with many tests. I believe that there is a cyclic reference issue created here. If the delegate is non-nil, it will not be released when the test ends. This does not cause a problem in isolation, but if for example, that delegate object (e.g. a view controller) also listens to notifications which are subsequently fired in other tests, you'll start to get all sorts of problems with non-deterministic tests. – jdmunro Jun 13 '14 at 08:52
  • @jdmunro help fixing it? – dB. Jun 18 '14 at 18:35
  • I haven't worked out a solution yet - did you experience the same issues? – jdmunro Jun 19 '14 at 16:13
  • OMG. I have been experiencing the issue mentioned by @jdmunro several times for the last couple of months. I had to nil the view controller after the test and make sure that I removed the VC from listening to notifications in viewDidDisappear. I then call the viewDidDisappear method in the tear down method of the test. Thanks jdmunro for highlighting this :) – Abdalrahman Shatou Oct 26 '14 at 10:10
2

I had issues with mocking UIAlertView as well, and my best guess is that it's the vararg that's throwing it off (can't 100% remember though). My solution was to create a factory method for UIAlertView and add it as a category.

+ (instancetype)alertViewWithTitle:(NSString *)title message:(NSString *)message delegate:(id)delegate cancelButtonTitle:(NSString *)cancelButtonTitle otherButtonTitles:(NSArray *)otherButtonTitles;

Notice that I replace the varargs with an NSArray. This method is definitely mockable, and the syntax is pretty similar now that we have array literals:

[UIAlertView alertViewWithTitle:@"Warning" message:@"Really delete your save file?" delegate:self cancelButtonTitle:@"No" otherButtonTitles:@[ @"Yes", @"Maybe" ]];

If you have the flexibility to change your source code, this'd be my suggestion.

EDIT

Looking more closely at your code, you are creating a partial mock, stubbing it's init method, then not doing anything with it. It's possible the way you are doing it might actually work if you replace the [UIAlertView alloc] with the mock you create. Can't say for sure because I do remember having issues with it.

Ben Flynn
  • 18,524
  • 20
  • 97
  • 142
0

For some reason mocking +(id)alloc in UIAlertView doesn't seem to work, so rather than partially mock UIAlertView and stub the (for example) initWithTitle: method, I now use the following fix. Hopefully this will be useful to anyone else facing similar problems.

XCTest_UIAlertView+MyCustomCategory.m

/**
    Tests alert displays on screen with correct message
    Method: +(void)showAlertWithMessage:
*/
-(void)test_showAlertWithMessage
{
    NSString *alertMessage = @"hello";

    UIAlertView *alert = [UIAlertView new];
    [UIAlertView setOCMock_UIAlertView:alert];

    id alertToTest = [OCMockObject partialMockForObject:alert];
    [[alertToTest expect] show];

    [UIAlertView showAlertWithMessage:alertMessage];

    [alertToTest verify];

    XCTAssert([alert.message isEqualToString:alertMessage], @"alert message incorrect, expected [%@]", alertMessage);
}

UIAlertView+MyCustomCategory.m

/**
 @warning variable for unit testing only
 */
static UIAlertView *__OCMock_UIAlertView;

@implementation UIAlertView (MyCustomCategory)

+(void)setOCMock_UIAlertView:(UIAlertView *)alert
{
    __OCMock_UIAlertView = alert;
}

-(id)init
{
    if(__OCMock_UIAlertView) 
    {
        self = __OCMock_UIAlertView;
        if(self) {
        }
        return self;
    }

    return [super init];
}

+(void)showAlertWithMessage:(NSString *)message
{
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil
                                                    message:message
                                                   delegate:nil
                                          cancelButtonTitle:@"ok"
                                          otherButtonTitles:nil];
    [alert show]; 
}
larromba
  • 306
  • 2
  • 11