以编程方式触发 UIAlertController 上的 UIAlertAction?

Trigger UIAlertAction on UIAlertController programmatically?

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

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

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

我大致是这样的:

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

  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)

我使用上面 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“)
    }
}

一种不涉及更改生产代码以允许在单元测试中以编程方式点击 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])
    }
}

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

typealias UIAlertActionHandler = (UIAlertAction) -> Void

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

具体object(已键入标题以便以后轻松检索):

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
}