I took a slightly different approach based on a tactic I took for testing UIContextualAction
—it's very similar to UIAction
but exposes its handler
as a property (not sure why Apple wouldn't have done the same for UIAction
). I injected an alert actions provider (encapsulated by a protocol) into my view controller. In production code, the former just vends the actions. In unit tests, I use a subclass of this provider which stores the action and the handler in two dictionaries—these can be queried and then triggered in tests.
typealias UIAlertActionHandler = (UIAlertAction) -> Void
protocol UIAlertActionProviderType {
func makeAlertAction(type: UIAlertActionProvider.ActionTitle, handler: UIAlertActionHandler?) -> UIAlertAction
}
Concrete object (has typed titles for easy retrieval later):
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)
}
}
Unit testing subclass (stores actions and handlers keyed by ActionTitle
enum):
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
}
}
Extension on UIAlertAction
to enable typed action title lookup in tests:
extension UIAlertAction {
var typedTitle: UIAlertActionProvider.ActionTitle? {
guard let title = title else { return nil }
return UIAlertActionProvider.ActionTitle(rawValue: title)
}
}
Sample test demonstrating usage:
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
}