Using OCUnit to test if an UIAlertView is presented
Asked Answered
O

4

6

I'm working on an app that will display a UIAlertView upon hitting it's exit button, only if progress in the game has been made. I was wondering how you would use OCUnit to intercept the UIAlertView and interact with it, or even detect if it has been presented. The only thing I can think of is to monkeypatch [UIAlertViewDelegate willPresentAlertView], but that makes me want to cry.

Does anyone know of a better method of doing this?

Outskirts answered 22/5, 2011 at 5:36 Comment(0)
B
4

Update: See my blog post How to Unit Test Your Alerts and Action Sheets

The problem with my other answer is that the -showAlertWithMessage: method itself is never exercised by unit tests. "Use manual testing to verify it once" isn't too bad for easy scenarios, but error handling often involves unusual situations that are difficult to reproduce. …Besides, I got that nagging feeling that I had stopped short, and that there might be a more thorough way. There is.

In the class under test, don't instantiate UIAlertView directly. Instead, define a method

+ (Class)alertViewClass
{
    return [UIAlertView class];
}

that can be replaced using "subclass and override." (Alternatively, use dependency injection and pass this class in as an initializer argument.)

Invoke this to determine the class to instantiate to show an alert:

Class alertViewClass = [[self class] alertViewClass];
id alert = [[alertViewClass alloc] initWithTitle:...etc...

Now define a mock alert view class. Its job is to remember its initializer arguments, and post a notification, passing itself as the object:

- (void)show
{
    [[NSNotificationCenter defaultCenter] postNotificationName:MockAlertViewShowNotification
                                                        object:self
                                                      userInfo:nil];
}

Your testing subclass (TestingFoo) redefines +alertViewClass to substitute the mock:

+ (Class)alertViewClass
{
    return [MockAlertView class];
}

Make your test class register for the notification. The invoked method can now verify the arguments passed to the alert initializer and the number of times -show was messaged.

Additional tip: In addition to the mock alert, I defined an alert verifier class that:

  • Registers for the notification
  • Lets me set expected values
  • Upon notification, verifies the state against the expected values

So all my alert tests do now is create the verifier, set the expectations, and exercise the call.

Briareus answered 30/5, 2011 at 4:21 Comment(4)
I think class posing would be more useful here. I get an icky feeling if my unit testing framework gets in the way of my real code. I could write a UIAlertViewPoser that sets a flag when an alert is displayed. cocoadev.com/index.pl?ClassPosingOutskirts
@wjlafrance Interesting, and would be great if it worked. Unfortunately, poseAsClass: has been deprecated on the Mac, and never supported on iOS.Briareus
Yeah, I noticed that shortly after I posted. Oh well.. :(Outskirts
For anyone using OCMock: if you define alertViewClass as an instance method, you could use a partial mock for your object under test to make it return the MockAlertView class. This removes the need to have a TestingFoo subclass.Banter
D
4

The latest version of OCMock (2.2.1 the at time of this writing) has features that make this easy. Here's some sample test code that stubs UIAlertView's "alloc" class method to return a mock object instead of a real UIAlertView.

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

[myViewController doSomething];

[mockAlertView verify];
Defoe answered 18/11, 2013 at 18:45 Comment(2)
Thanks! Man, I remember testing before I learned about mocks. Those were dark ages.Outskirts
Can I use the same approach for mail composer? Checkout my question: #21969056Feeding
B
3

Note: Please see my other answer. I recommend it over this one.

In the actual class, define a short method to show an alert, something like:

- (void)showAlertWithMessage:(NSString message *)message
{
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil
                                                    message:message
                                                   delegate:self
                                          cancelButtonTitle:@"OK"
                                          otherButtonTitles:nil];
    [alert show];
    [alert release];
}

For your test, don't test this actual method. Instead, use "subclass and override" to define a spy that simply records its calls and arguments. Let's say the original class is named "Foo". Here's a subclass for testing purposes:

@interface TestingFoo : Foo
@property(nonatomic, assign) NSUInteger countShowAlert;
@property(nonatomic, retain) NSString *lastShowAlertMessage;
@end

@implementation TestingFoo
@synthesize countShowAlert;
@synthesize lastShowAlertMessage;

- (void)dealloc
{
    [lastShowAlertMessage release];
    [super dealloc];
}

- (void)showAlertWithMessage:(NSString message *)message
{
    ++countShowAlert;
    [self setLastShowAlertMessage:message];
}

@end

Now as long as

  • your code calls -showAlertWithMessage: instead of showing an alert directly, and
  • your test code instantiates TestingFoo instead of Foo,

you can check the number of calls to show an alert, and the last message.

Since this doesn't exercise the actual code that shows an alert, use manual testing to verify it once.

Briareus answered 22/5, 2011 at 6:27 Comment(2)
does the subclass and override paradigm work if your Obj C class is already a subclass of the test framework, e.g. SenTestCase ? Would the only alternative be dependency injection?Stasny
I don't see why you shouldn't be able to subclass any concrete class you define yourself, in order to override a specific method.Briareus
B
0

You can get unit tests for alert views fairly seamlessly by exchanging the 'show' implementation of UIAlertView. For example, this interface gives you some amount of testing abilities:

@interface UIAlertView (Testing)

+ (void)skipNext;
+ (BOOL)didSkip;

@end

with this implementation

#import <objc/runtime.h>
@implementation UIAlertView (Testing)

static BOOL skip = NO;

+ (id)alloc
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method showMethod  = class_getInstanceMethod(self, @selector(show));
        Method show_Method = class_getInstanceMethod(self, @selector(show_));
        method_exchangeImplementations(showMethod, show_Method);
    });
    return [super alloc];
}

+ (void)skipNext
{
    skip = YES;
}

+ (BOOL)didSkip
{
    return !skip;
}

- (void)show_
{
    NSLog(@"UIAlertView :: would appear here (%@) [ title = %@; message = %@ ]", skip ? @"predicted" : @"unexpected", [self title], [self message]);
    if (skip) {
        skip = NO;
        return;
    }
}

@end

You can write unit tests e.g. like this:

[UIAlertView skipNext];
// do something that you expect will give an alert
STAssertTrue([UIAlertView didSkip], @"Alert view did not appear as expected");

If you want to automate tapping a specific button in the alert view, you will need some more magic. The interface gets two new class methods:

@interface UIAlertView (Testing)

+ (void)skipNext;
+ (BOOL)didSkip;

+ (void)tapNext:(NSString *)buttonTitle;
+ (BOOL)didTap;

@end

which go like this

static NSString *next = nil;

+ (void)tapNext:(NSString *)buttonTitle
{
    [next release];
    next = [buttonTitle retain];
}

+ (BOOL)didTap
{
    BOOL result = !next;
    [next release];
    next = nil;
    return result;
}

and the show method becomes

- (void)show_
{
    if (next) {
        NSLog(@"UIAlertView :: simulating alert for tapping %@", next);
        for (NSInteger i = 0; i < [self numberOfButtons]; i++) 
            if ([next isEqualToString:[self buttonTitleAtIndex:i]]) {
                [next release];
                next = nil;
                [self alertView:self clickedButtonAtIndex:i];
                return;
            }
        return;
    }
    NSLog(@"UIAlertView :: would appear here (%@) [ title = %@; message = %@ ]", skip ? @"predicted" : @"unexpected", [self title], [self message]);
    if (skip) {
        skip = NO;
        return;
    }
}

This can be tested similarly, but instead of skipNext you'd say which button to tap. E.g.

[UIAlertView tapNext:@"Download"];
// do stuff that triggers an alert view with a "Download" button among others
STAssertTrue([UIAlertView didTap], @"Download was never tappable or never tapped");
Barrens answered 30/6, 2013 at 14:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.