以编程方式触发 UIAlertController 上的 UIAlertAction?
Trigger UIAlertAction on UIAlertController programmatically?
关于这个主题有几个现有的问题,但它们并不是我想要的。我为我的应用程序编写了一些 Swift 应用程序评级提示,其中显示了两个 UIAlertController
实例,一个由另一个触发。
我现在正尝试对此进行单元测试,并尝试在测试中达到第二个警报。我已经编写了一个简单的间谍程序来检查第一个控制器,但我想要一种方法来触发第一个警报中的一个操作,然后显示第二个。
我已经尝试过 alert.actions.first?.accessibilityActivate()
,但它似乎并没有破坏该操作的处理程序 – 这就是我所追求的。
我大致是这样的:
创建了我的 class 的模拟版本,它将显示警报控制器,并在我的单元测试中使用了这个模拟。
覆盖我在非模拟版本中创建的以下方法:
func alertActionWithTitle(title: String?, style: UIAlertActionStyle, handler: Handler) -> UIAlertAction
在重写的实现中,在某些属性中存储了有关操作的所有详细信息(Handler
只是一个类型别名 () -> (UIAlertAction)
)
var didCreateAlert = false
var createdTitles: [String?] = []
var createdStyles: [UIAlertActionStyle?] = []
var createdHandlers: [Handler?] = []
var createdActions: [UIAlertAction?] = []
然后,当 运行 我的测试时,为了通过警报遍历路径,我实现了一个 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
}
关于这个主题有几个现有的问题,但它们并不是我想要的。我为我的应用程序编写了一些 Swift 应用程序评级提示,其中显示了两个 UIAlertController
实例,一个由另一个触发。
我现在正尝试对此进行单元测试,并尝试在测试中达到第二个警报。我已经编写了一个简单的间谍程序来检查第一个控制器,但我想要一种方法来触发第一个警报中的一个操作,然后显示第二个。
我已经尝试过 alert.actions.first?.accessibilityActivate()
,但它似乎并没有破坏该操作的处理程序 – 这就是我所追求的。
我大致是这样的:
创建了我的 class 的模拟版本,它将显示警报控制器,并在我的单元测试中使用了这个模拟。
覆盖我在非模拟版本中创建的以下方法:
func alertActionWithTitle(title: String?, style: UIAlertActionStyle, handler: Handler) -> UIAlertAction
在重写的实现中,在某些属性中存储了有关操作的所有详细信息(
Handler
只是一个类型别名() -> (UIAlertAction)
)var didCreateAlert = false var createdTitles: [String?] = [] var createdStyles: [UIAlertActionStyle?] = [] var createdHandlers: [Handler?] = [] var createdActions: [UIAlertAction?] = []
然后,当 运行 我的测试时,为了通过警报遍历路径,我实现了一个
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
}