测试/模拟从视图控制器上的方法内部调用为类方法的 UIAlertView

Posted

技术标签:

【中文标题】测试/模拟从视图控制器上的方法内部调用为类方法的 UIAlertView【英文标题】:test / mocking for a UIAlertView called as class method from inside of method on view controller 【发布时间】:2015-08-15 18:01:33 【问题描述】:

我正在为视图控制器中的 performLogin 方法编写测试,我无法重构原始方法,也无法重构项目中的任何其他文件。我只能修改我的 LoginTest.m 文件。我正在尝试使用 OCMock 在不更改原始程序文件的情况下完成此测试。

我正在尝试验证是否在 performLogin 方法收到异常时显示 UIAlertView。 ViewHelper 类有一个类方法,它返回 void 并创建和显示 UIAlertView。

我已经模拟了调用 ViewHelper 的视图控制器,并且我尝试使用 OCMock 使用来自各种搜索的示例来测试 UIAlertView 类,但是这些测试失败了,因为它没有捕捉到 AlertView 的创建。

我是使用 OCMock 的新手,我不确定如何为此类测试模拟 ViewHelper 文件,尤其是当它是从方法内调用而不是锚定到属性时。

可能有一种简单的方法可以做到这一点,但我还没有弄清楚,如果有任何帮助,我们将不胜感激。

提前致谢。

在调用doLogin的方法内部,它返回TRUE或抛出异常。当异常被抛出时,它应该弹出一个带有消息的 UIAlertView。我想测试这部分代码并验证 UIAlertView 是否实际显示。

请注意,ViewHelper 是直接从 catch 块中调用的。

方法如下:

-(IBAction)performLogin:(id)sender 
  if(valid) 
  
    @try
    
      //loginModel is a private instance variable
      loginModel = [[Services sharedInstance] getLoginModel];
      [loginModel doLoginWithUsername:username andPassword:password];
      [self performSegueWithIdentifier:@"showWelcomeScreen" sender:self];
    
    @catch(NSException *e)
    

      //I NEED TO TEST THAT THIS CALL OCCURS AND THE UIAlertView is shown
      //Not sure how to do this as it's not anchored to a property or method
      //and is called as a class method

      [ViewHelper showAlertForTitle:"Error" andTheMessage:@"network error"
        andAccessibilityLabel:@"Network Error"];
    
   

ViewHelpder.m 看起来像这样 - 注意,它是一个类方法:

+(void) showAlertForTitle:(NSString *)title andTheMessage:(NSString *)message      andAccessibilityLabel:(NSString *)label

    NSLog(@"reached alert notification class");
    UIAlertView *successAlert = [[UIAlertView alloc] initWithTitle:title  message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil, nil];
    [successAlert setAccessibilityLabel:label];
    [successAlert show];

到目前为止,我已经尝试过:

- (void)testForfailedLogin 

    //call singleton first to initialize mocked login model
    [[Services sharedInstance] getLoginModel];

    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];


    //set the actual IBOutlets with login values designed to sucessfully login
    self.controller.usernameTextField.text = @"error";
    self.controller.passwordTextField.text = @"error";

    //call the login process to verify the login method
    [self.controller performLogin:nil];

    [mockAlertView verify];
    [mockAlertView stopMocking];

【问题讨论】:

【参考方案1】:

正如 nacho4d 的回答中提到的,这有两个部分:使登录模型抛出异常并验证控制器是否显示错误。

鉴于您描述的约束,我会为登录模型使用部分模拟来引发异常:

id modelMock = OCMPartialMock([[Services sharedInstance] getLoginModel]);
OCMStub([modelMock doLoginWithUsername:[OCMArg any] andPassword:[OCMArg any]])
   .andThrow([NSException exceptionWithName:@"TestException" reason:@"Test" userInfo:nil]);

要检查面板是否显示,我不会打扰视图。我只需检查帮助程序中的类方法是从performLogin: 实现中调用的:

id viewHelperMock = OCMClassMock([ViewHelper class]);
[controller performLogin:sender]; 
OCMVerify([viewHelperMock showAlertForTitle:"Error" andTheMessage:@"network error" andAccessibilityLabel:@"Network Error"]);

【讨论】:

谢谢。我的问题最初是我的异常设置很好,但是我忘记了包装 doLogin 调用的用户名/密码验证检查。我必须做一个 partialMock 并存根该调用以始终返回 true,以允许调用 doLogin。在那里,我能够微调真值或异常的返回。 很高兴直接从 OCMock 的创建者那里得到建议 :)【参考方案2】:

我不记得 OCMock api 了,所以这肯定不会编译,但我相信这个想法是正确的。 (我基本上是在关注this guide)

首先,最好用两个测试来测试它。 第一个测试将仅检查 [ViewHelper showAlertForTitle:"Error" andTheMessage:@"network error" andAccessibilityLabel:@"Network Error"] 在异常时被调用。第二个测试将检查在调用+[ViewHelper showAlertForTitle:andTheMessage:andAccessibilityLabel:] 时出现的警报视图(或您希望发生的任何其他事情)。

第一个测试的(重要部分):

// ① You need to setup something to raise an exception
// so the catch block will be called
// Probably you want to use [OCMArg any] or [OCMArg anyPointer]
OCMStub([mockServices loginWithUsername:[OCMArg isNotNil]
                            andPassword:[OCMArg isNotNil]])
  .andThrow(anException);

// ② Call your method
// sender can be nil in your code.
[controller performLogin:sender]; 

// ③ Expect your method to be called
OCMExpect([mockViewHelper showAlertForTitle:"Error" 
                              andTheMessage:@"network error"
                      andAccessibilityLabel:@"Network Error"]);

// ④ Verify
OCMVerifyAll(mockViewHelper)

第二次测试的要点:

// ① Stub everything you want to check to be called inside 
// `+[ViewHelper showAlertForTitle:andTheMessage:andAccessibilityLabel:]`
// probably your UIAlertView, etc
id mockAlertView = ...

// ② Call your method
[ViewHelper showAlertForTitle:@"something" 
                andTheMessage:@"something" 
        andAccessibilityLabel:@"something"]; 

// ③ Expect your stuff to be called
OCMExpect([mockAlertView someMethod]);

// ④ Verify
OCMVerifyAll(mockAlertView)

最后一件事,在 objc 世界中,异常用于程序员错误,如果您正在设计这些类,请考虑像许多其他 Cocoa API 一样返回 NSError。

【讨论】:

感谢您的帮助,它触发了我需要解决的问题。

以上是关于测试/模拟从视图控制器上的方法内部调用为类方法的 UIAlertView的主要内容,如果未能解决你的问题,请参考以下文章

模拟另一个类方法的内部调用

Grails:我如何模拟被测类的其他方法,这些方法可能在测试期间被内部调用

PHPUnit:如何通过测试方法模拟内部调用的函数

以正确的方式从视图中调用控制器方法

Iphone UIView 父方法调用

单元测试控制器时无法模拟 Grails 服务方法 - MissingMethodException