OCMock - partial mocking [UIAlertView alloc]

2019-07-07 01:40发布

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?

3条回答
放我归山
2楼-- · 2019-07-07 02:25

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.

查看更多
地球回转人心会变
3楼-- · 2019-07-07 02:30

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]; 
}
查看更多
做个烂人
4楼-- · 2019-07-07 02:33

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.

查看更多
登录 后发表回答