【问题标题】:Trigger UIAlertAction on UIAlertController programmatically?以编程方式在 UIAlertController 上触发 UIAlertAction?
【发布时间】:2016-03-23 09:03:13
【问题描述】:

关于这个主题有几个现有的问题,但它们并不是我想要的。我为我的应用编写了一个小的 Swift 应用评分提示,它显示了两个 UIAlertController 实例,一个由另一个触发。

我现在正在尝试对此进行单元测试,并尝试在测试中达到第二个警报。我编写了一个简单的 spy 来检查第一个控制器,但我想要一种方法来触发第一个警报上的一个动作,然后显示第二个。

我已经尝试过alert.actions.first?.accessibilityActivate(),但它似乎并没有在该操作的处理程序内部中断——这就是我所追求的。

【问题讨论】:

    标签: swift unit-testing uialertcontroller uialertaction


    【解决方案1】:

    一种不涉及更改生产代码以允许在单元测试中以编程方式利用 UIAlertActions 的解决方案,我发现了 in this SO answer

    在这里发布它以及在谷歌搜索答案时弹出这个问题,以下解决方案让我花了更多时间找到。

    在您的测试目标中添加以下扩展:

    extension UIAlertController {
        typealias AlertHandler = @convention(block) (UIAlertAction) -> Void
    
        func tapButton(atIndex index: Int) {
            guard let block = actions[index].value(forKey: "handler") else { return }
            let handler = unsafeBitCast(block as AnyObject, to: AlertHandler.self)
            handler(actions[index])
        }
    }
    

    【讨论】:

    • 这应该是选择的答案
    • 谢谢,工作!由于按钮的索引可能会更改,您可以通过标题来识别它们,例如:func tapButton(title: String) { guard let action = actions.first(where: {$0.title == title}), let block = action.value(forKey: "handler") else { return } let handler = unsafeBitCast(block as AnyObject, to: AlertHandler.self) handler(action) }
    • 我在网上收到caught "NSInvalidArgumentException", "Source type 1 not available" handler(actions[index])
    • @SchmidtFx 看起来您正在使用 UIImagePickerController?这段代码应该适用于 UIAlertController,我没有在 UIImagePicker 上测试过
    • @atereshkov,是的,我想在执行 UIAlertAction 处理程序时显示 UIImagePickerController
    【解决方案2】:

    这大概是我所做的:

    1. 为我的类创建了一个模拟版本,它可以显示警报控制器,并在我的单元测试中使用了这个模拟。

    2. 覆盖我在非模拟版本中创建的以下方法:

      func alertActionWithTitle(title: String?, style: UIAlertActionStyle, handler: Handler) -> UIAlertAction
      
    3. 在重写的实现中,将有关操作的所有详细信息存储在某些属性中(Handler 只是一个类型别名() -> (UIAlertAction)

      var didCreateAlert = false
      var createdTitles: [String?] = []
      var createdStyles: [UIAlertActionStyle?] = []
      var createdHandlers: [Handler?] = []
      var createdActions: [UIAlertAction?] = []
      
    4. 然后,在运行测试时,为了遍历警报路径,我实现了一个 callHandlerAtIndex 方法来遍历我的处理程序并执行正确的处理程序。

    这意味着我的测试看起来像这样:

    feedback.start()
    feedback.callHandlerAtIndex(1) // First alert, second action
    feedback.callHandlerAtIndex(2) // Second alert, third action
    XCTAssertTrue(mockMailer.didCallMail)
    

    【讨论】:

    • 看看最受好评的答案,非常好!
    【解决方案3】:

    基于我测试UIContextualAction 所采用的策略,我采取了一种稍微不同的方法——它与UIAction 非常相似,但将其handler 公开为一个属性(不知道为什么Apple 不会这样做UIAction)。我将一个警报操作提供程序(由协议封装)注入到我的视图控制器中。在生产代码中,前者只是提供操作。在单元测试中,我使用了这个提供者的一个子类,它将动作和处理程序存储在两个字典中——它们可以被查询然后在测试中触发。

    typealias UIAlertActionHandler = (UIAlertAction) -> Void
    
    protocol UIAlertActionProviderType {
        func makeAlertAction(type: UIAlertActionProvider.ActionTitle, handler: UIAlertActionHandler?) -> UIAlertAction
    }
    

    具体对象(已键入标题以便稍后检索):

    class UIAlertActionProvider: UIAlertActionProviderType {
        enum ActionTitle: String {
            case proceed = "Proceed"
            case cancel = "Cancel"
        }
    
        func makeAlertAction(title: ActionTitle, handler: UIAlertActionHandler?) -> UIAlertAction {
            let style: UIAlertAction.Style
            switch title {
            case .proceed: style = .destructive
            case .cancel: style = .cancel
            }
    
            return UIAlertAction(title: title.rawValue, style: style, handler: handler)
        }
    }
    

    单元测试子类(存储由ActionTitle 枚举键入的操作和处理程序):

    class MockUIAlertActionProvider: UIAlertActionProvider {
        var handlers: [ActionTitle: UIAlertActionHandler] = [:]
        var actions: [ActionTitle: UIAlertAction] = [:]
    
        override func makeAlertAction(title: ActionTitle, handler: UIAlertActionHandler?) -> UIAlertAction {
            handlers[title] = handler
    
            let action = super.makeAlertAction(title: title, handler: handler)
            actions[title] = action
    
            return action
        }
    }
    

    UIAlertAction 上的扩展以在测试中启用键入的动作标题查找:

    extension UIAlertAction {
        var typedTitle: UIAlertActionProvider.ActionTitle? {
            guard let title = title else { return nil }
    
            return UIAlertActionProvider.ActionTitle(rawValue: title)
        }
    }
    

    演示用法的示例测试:

    func testDeleteHandlerActionSideEffectTakesPlace() throws {
        let alertActionProvider = MockUIAlertActionProvider()
        let sut = MyViewController(alertActionProvider: alertActionProvider)
    
        // Do whatever you need to do to get alert presented, then retrieve action and handler
        let action = try XCTUnwrap(alertActionProvider.actions[.proceed])
        let handler = try XCTUnwrap(alertActionProvider.handlers[.proceed])
        handler(action)
    
        // Assert whatever side effects are triggered in your code by triggering handler
    }
    

    【讨论】:

      【解决方案4】:

      我使用上面 Luke 的指导创建了一个 UIAlertAction 的子类,它保存了它的完成块,以便在测试期间可以调用它:

      class BSAlertAction: UIAlertAction {
      
          var completionHandler: ((UIAlertAction) -> Swift.Void)?
      
          class func handlerSavingAlertAction(title: String?,
                                              style: UIAlertActionStyle,
                                              completionHandler: @escaping ((UIAlertAction) -> Swift.Void)) -> BSAlertAction {
              let alertAction = self.init(title: title, style: style, handler: completionHandler)
              alertAction.completionHandler = completionHandler
              return alertAction
          }
      
      }
      

      如果您愿意,您可以自定义此项以保存更多信息(如标题和样式)。下面是一个使用此实现的 XCTest 示例:

      func testThatMyMethodGetsCalled() {
          if let alert = self.viewController?.presentedViewController as? UIAlertController,
              let action = alert.actions[0] as? BSAlertAction,
              let handler = action.completionHandler {
                  handler(action)
                  let calledMyMethod = self.presenter?.callTrace.contains(.myMethod) ?? false
                  XCTAssertTrue(calledMyMethod)
          } else {
              XCTFail("Got wrong kind of alert when verifying that my method got called“)
          }
      }
      

      【讨论】:

      • 我为此考虑了对UIAlertAction 的子类化,因为在某种程度上感觉这是缺少的功能(基于UIContextualAction),但最后我走了一条不同的路线。你能告诉我你正在使用的callTrace 属性是什么吗?
      • 哎呀,对不起,是的,我应该在那里更清楚。在这段代码中,callTrace 是一个数组,用于存储我们代码中函数名称的枚举表示。逻辑是,当一个函数被调用时,我们可以在callTrace 中附加它的名称/对应的枚举,然后,当我们在代码的其他地方执行我们的断言时,我们可以查询callTrace 来验证我们期望的方法被称为实际上是。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-05-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-01-01
      • 2019-04-04
      相关资源
      最近更新 更多