Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trigger UIAlertAction on UIAlertController programmatically?

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.

like image 643
Luke Avatar asked Mar 23 '16 09:03

Luke


2 Answers

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)
like image 168
Luke Avatar answered Nov 15 '22 21:11

Luke


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])
    }
}
like image 28
mmvdv Avatar answered Nov 15 '22 22:11

mmvdv