12

There are a couple of existing questions on this topic but they aren't quite what I'm after. I've written a little Swift app rating prompt for my app which presents two UIAlertController instances, one triggered by the other.

I'm now trying to unit test this, and trying to reach that second alert in the tests. I've written a simple spy to check the first controller, but I'd like a way to trigger one of the actions on the first alert, which in turn shows the second.

I've already tried alert.actions.first?.accessibilityActivate(), but it didn't seem to break inside the handler of that action – that's what I'm after.

4

4 回答 4

26

一种不涉及更改生产代码以允许在单元测试中以编程方式点击 UIAlertActions 的解决方案,我在此 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])
    }
}
于 2019-01-09T13:53:55.350 回答
5

这大致是我所做的:

  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)
于 2016-04-05T07:42:32.307 回答
0

I used Luke's guidance above to create a subclass of UIAlertAction that saves its completion block so it can be called during tests:

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
    }

}

You could customize this to save more information (like the title and the style) if you like. Here's an example of an XCTest that then uses this implementation:

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“)
    }
}
于 2018-09-23T14:08:18.773 回答
0

根据我用于测试的策略,我采用了一种稍微不同的方法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
}
于 2020-08-08T03:00:52.550 回答