How To Test UIAlertController in Swift
Update Nov. 25th 2015: This kind of testing does not work. You can find the correct version here.
Recently I read a blog
post about
testing UIAlertController
in
Objective-C using control swizzling. Posts like this always trigger me
to find a way to test the same without the swizzling. I know that
swizzling is a powerful tool developers should have access to in their
developer tool box. But I personally avoid it when ever I can. In fact
only one app I worked on in the last six years used swizzling. And today
I believe we could have implemented it without it.
So how to test UIAlertController
in
Swift without the swizzling?
Let’s start with the code we want to test. I have added a button to the storyboard. (I’m using a storyboard here to make this post accessible to people who don’t wan’t to do their UI in code.) When the button is tapped an alert is shown with a title, a message and two buttons, OK and Cancel. Here is the code:
import UIKit
class ViewController: UIViewController {
var actionString: String?
@IBAction func showAlert(sender: UIButton) {
let alertViewController = UIAlertController(title: "Test Title", message: "Message", preferredStyle: .Alert)
let okAction = UIAlertAction(title: "OK", style: .Default) { (action) -> Void in
self.actionString = "OK"
}
let cancelAction = UIAlertAction(title: "Cancel", style: .Cancel) { (action) -> Void in
self.actionString = "Cancel"
}
alertViewController.addAction(cancelAction)
alertViewController.addAction(okAction)
presentViewController(alertViewController, animated: true, completion: nil)
}
}
Note that the alert actions don’t do anything interesting in this example. They just represent a change a unit test could verify.
Let’s start with an easy test: Test the title and the message of the alert controller.
The setup code for the tests looks like this:
import XCTest
@testable import TestingAlertExperiment
class TestingAlertExperimentTests: XCTestCase {
var sut: ViewController!
override func setUp() {
super.setUp()
sut = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! ViewController
UIApplication.sharedApplication().keyWindow?.rootViewController = sut
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
}
We need to set the sut to the rootViewController otherwise the view controller can’t present the alert view controller.
Add the following test for the title of a UIAlertController:
func testAlert_HasTitle() {
sut.showAlert(UIButton())
XCTAssertTrue(sut.presentedViewController is UIAlertController)
XCTAssertEqual(sut.presentedViewController?.title, "Test Title")
}
That was easy. Now let’s test the cancel button of the UIAlertController. The problem: The handler block of the alert action can’t be accessed. Therefore we need a mock alert action to store the handler to be able to call it in the test to see if the action does what we expect. Add the following mock class within the test case:
class MockAlertAction : UIAlertAction {
typealias Handler = ((UIAlertAction) -> Void)
var handler: Handler?
var mockTitle: String?
var mockStyle: UIAlertActionStyle
convenience init(title: String?, style: UIAlertActionStyle, handler: ((UIAlertAction) -> Void)?) {
self.init()
mockTitle = title
mockStyle = style
self.handler = handler
}
override init() {
mockStyle = .Default
super.init()
}
}
The primary job of the mock class is to capture the handler for later use. Now we need to inject the mock class into the implementation code. Replace the view controller code with the following:
import UIKit
class ViewController: UIViewController {
var Action = UIAlertAction.self
var actionString: String?
@IBAction func showAlert(sender: UIButton) {
let alertViewController = UIAlertController(title: "Test Title", message: "Message", preferredStyle: .Alert)
let okAction = Action.init(title: "OK", style: .Default) { (action) -> Void in
self.actionString = "OK"
}
let cancelAction = Action.init(title: "Cancel", style: .Cancel) { (action) -> Void in
self.actionString = "Cancel"
}
alertViewController.addAction(cancelAction)
alertViewController.addAction(okAction)
presentViewController(alertViewController, animated: true, completion: nil)
}
}
We have added a class variable Action
which is set
to UIAlertAction.self
.
This variable is used when we initialize the alert actions. This enables
us to overwrite it in the test. Let’s do it:
func testAlert_FirstActionStoresCancel() {
sut.Action = MockAlertAction.self
sut.showAlert(UIButton())
let alertController = sut.presentedViewController as! UIAlertController
let action = alertController.actions.first as! MockAlertAction
action.handler!(action)
XCTAssertEqual(sut.actionString, "Cancel")
}
First we inject the mock alert action. Then we call the code that presents the alert view controller. We get the cancel action from the presented view controller and call the captured handler. The last step is to assert that the action actually does what we expect.
That’s it. A very easy way to test an UIAlertViewController
without the
swizzling.
Update Nov. 25th 2015: This kind of testing does not work. You can find the correct version here.