Trigger UIAlertAction on UIAlertController programmatically?

A solution that doesn't involve changing the production code to allow programmatic tapping of UIAlertActions in unit tests, which I found in this SO answer.

Posting it here as well as this question popped up for me when Googling for an answer, and the following solution took me way more time to find.

Put below extension in your test target:

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])
    }
}

Here's roughly what I did:

  1. Created a mocked version of my class that would present the alert controller, and in my unit tests, used this mock.

  2. Overrode the following method that I'd created in the non-mocked version:

    func alertActionWithTitle(title: String?, style: UIAlertActionStyle, handler: Handler) -> UIAlertAction
    
  3. In the overridden implementation, stored all the details about the actions in some properties (Handler is just a typealias'd () -> (UIAlertAction))

    var didCreateAlert = false
    var createdTitles: [String?] = []
    var createdStyles: [UIAlertActionStyle?] = []
    var createdHandlers: [Handler?] = []
    var createdActions: [UIAlertAction?] = []
    
  4. Then, when running my tests, to traverse the path through the alerts, I implemented a callHandlerAtIndex method to iterate through my handlers and execute the right one.

This means that my tests look something like this:

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