Two month ago I published a post about How To Test UIAlertController. A reader found out that the test doesn’t work as I expected:

@dasdom Your tests work, but ur convienience init in MockUIAction is never triggered - u can’t override convienience inits. Seems an ios bug

— Larhythimx (@Larhythmix) 25. November 2015

Larhythimx is totally right. The init method of the mock is never called. The reason why I didn’t see this when I wrote the test is, that the handler is actually called. It looks like the real UIAlertAction does use handler as the hidden internal variable to store the handler closure of the action. This is fragil and Larhythimx mentions in another tweet that the handler is nil in test he tries to write.

So as the golden way (i.e. write tests without changing the implementation) does not work here, let’s go for silver.

First we add a class method to UIAlertAction that creates actions. Add the following extension in ViewController.swift :

extension UIAlertAction {
  class func makeActionWithTitle(title: String?, style: UIAlertActionStyle, handler: ((UIAlertAction) -> Void)?) -> UIAlertAction {
    return UIAlertAction(title: title, style: style, handler: handler)
  }
}

In the MockAlertAction add this override:

override class func makeActionWithTitle(title: String?, style: UIAlertActionStyle, handler: ((UIAlertAction) -> Void)?) -> MockAlertAction {
  return MockAlertAction(title: title, style: style, handler: handler)
}

In the implementation code we can now use the class methods to create the alert actions:

let okAction = Action.makeActionWithTitle("OK", style: .Default) { (action) -> Void in
    self.actionString = "OK"
}
let cancelAction = Action.makeActionWithTitle("Cancel", style: .Default) { (action) -> Void in
    self.actionString = "Cancel"
}
alertViewController.addAction(cancelAction)

To make sure we do really test, what we think we do test, rename the handler property in MockAlertAction to mockHandler:

var mockHandler: Handler?

In addition we add tests for the mock title of the actions. The test for the cancel action then looks like this:

func testAlert_FirstActionStoresCancel() {
  sut.Action = MockAlertAction.self
  
  sut.showAlert(UIButton())
  
  let alertController = sut.presentedViewController as! UIAlertController
  let action = alertController.actions.first as! MockAlertAction
  action.mockHandler!(action)
  
  XCTAssertEqual(sut.actionString, "Cancel")
  XCTAssertEqual(action.mockTitle, "Cancel")
}

This test would have failed in the previous version because as the init method got never called the mock title did not get set.

You can find the corrected version on github.

Thanks again Larhythimx for the tweet!