Iphone 使用OCUnit测试是否显示UIAlertView

Iphone 使用OCUnit测试是否显示UIAlertView,iphone,unit-testing,uialertview,ocunit,Iphone,Unit Testing,Uialertview,Ocunit,我正在开发一款应用程序,只有在游戏取得进展的情况下,点击退出按钮时才会显示UIAlertView。我想知道您将如何使用OCUnit截取UIAlertView并与之交互,甚至检测它是否已呈现。我唯一能想到的就是monkeypatch[UIAlertViewDelegate willPresentAlertView],但这让我想哭 有人知道更好的方法吗?注意:请参阅我的其他答案。我推荐它胜过这个 在实际类中,定义一个简短的方法来显示警报,例如: - (void)showAlertWithMessag

我正在开发一款应用程序,只有在游戏取得进展的情况下,点击退出按钮时才会显示
UIAlertView
。我想知道您将如何使用OCUnit截取
UIAlertView
并与之交互,甚至检测它是否已呈现。我唯一能想到的就是monkeypatch
[UIAlertViewDelegate willPresentAlertView]
,但这让我想哭

有人知道更好的方法吗?

注意:请参阅我的其他答案。我推荐它胜过这个

在实际类中,定义一个简短的方法来显示警报,例如:

- (void)showAlertWithMessage:(NSString message *)message
{
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil
                                                    message:message
                                                   delegate:self
                                          cancelButtonTitle:@"OK"
                                          otherButtonTitles:nil];
    [alert show];
    [alert release];
}
对于你的测试,不要测试这个实际的方法。相反,使用“subclass和override”来定义一个只记录其调用和参数的间谍。假设原始类名为“Foo”。以下是用于测试目的的子类:

@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
现在只要

  • 您的代码调用
    -showAlertWithMessage:
    ,而不是直接显示警报,并且
  • 您的测试代码实例化了
    TestingFoo
    ,而不是
    Foo
您可以检查显示警报的呼叫数以及最后一条消息

由于这不会执行显示警报的实际代码,请使用手动测试对其进行一次验证。

更新:请参阅我的博客文章

我的另一个答案的问题是,
-showartwithmessage:
方法本身从来没有被单元测试执行过。“使用手动测试验证一次”对于简单的场景来说并不太糟糕,但错误处理通常涉及难以重现的异常情况…此外,我有一种唠叨的感觉,我突然停了下来,也许还有更彻底的办法。有

在测试的类中,不要直接实例化
UIAlertView
。相反,请定义一个方法

+ (Class)alertViewClass
{
    return [UIAlertView class];
}
可以使用“subclass and override”替换(或者,使用依赖项注入并将该类作为初始值设定项参数传入)

调用此命令以确定要实例化以显示警报的类:

Class alertViewClass = [[self class] alertViewClass];
id alert = [[alertViewClass alloc] initWithTitle:...etc...
现在定义一个模拟警报视图类。它的工作是记住其初始值设定项参数,并发布通知,将自身作为对象传递:

- (void)show
{
    [[NSNotificationCenter defaultCenter] postNotificationName:MockAlertViewShowNotification
                                                        object:self
                                                      userInfo:nil];
}
您的测试子类(TestingFoo)重新定义了
+alertViewClass
以替换模拟:

+ (Class)alertViewClass
{
    return [MockAlertView class];
}
让您的测试类注册通知。调用的方法现在可以验证传递给警报初始值设定项的参数以及发送消息的次数
-show

附加提示:除了模拟警报之外,我还定义了一个警报验证器类:

  • 登记通知
  • 让我设置期望值
  • 通知后,根据预期值验证状态

因此,我现在所做的所有警报测试都是创建验证器、设置期望值并执行调用。

通过交换UIAlertView的“show”实现,您可以非常无缝地获得警报视图的单元测试。例如,此界面为您提供了一些测试功能:

@interface UIAlertView (Testing)

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

@end
通过这种实现

#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
如果您想自动点击警报视图中的特定按钮,则需要更多的魔法。该接口获得两个新的类方法:

@interface UIAlertView (Testing)

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

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

@end
是这样的吗

static NSString *next = nil;

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

+ (BOOL)didTap
{
    BOOL result = !next;
    [next release];
    next = nil;
    return result;
}
而show方法就变成了

- (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;
    }
}

最新版本的OCMock(撰写本文时为2.2.1)具有使这一过程变得简单的功能。下面是一些示例测试代码,它将UIAlertView的“alloc”类方法存根为返回模拟对象而不是真实的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];

我认为在这里摆姿势更有用。如果我的单元测试框架妨碍了我真正的代码,我会有一种恶心的感觉。我可以编写一个UIAlertViewPoser,在显示警报时设置一个标志@wjlafrance很有趣,如果它能起作用那就太好了。不幸的是,poseAsClass:在Mac上被弃用,在iOS上也不受支持。是的,我在发布后不久就注意到了这一点。哦,好吧(对于任何使用OCMock的人:如果您将alertViewClass定义为实例方法,您可以对受测对象使用部分模拟,使其返回MockAlertView类。这样就不需要有TestingFoo子类。如果您的Obj C类已经是测试框架的子类,例如SenTe,那么子类和重写范例是否起作用stCase?唯一的替代方案是依赖注入吗?我不明白为什么你不能为自己定义的任何具体类创建子类,以覆盖特定的方法。谢谢!伙计,我记得在我学习Mock之前进行过测试。那是黑暗时代。我能对mail composer使用相同的方法吗?检查我的问题:
[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");
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];