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.
Here's roughly what I did:
Created a mocked version of my class that would present the alert controller, and in my unit tests, used this mock.
Overrode the following method that I'd created in the non-mocked version:
func alertActionWithTitle(title: String?, style: UIAlertActionStyle, handler: Handler) -> UIAlertAction
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?] = []
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)
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])
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With