Search code examples
swiftxctestuialertcontroller

Testing if UIAlertController has been presented


I have a protocol I use to allow my ViewControllers to present an alert.

import UIKit

struct AlertableAction {
    var title: String
    var style: UIAlertAction.Style
    var result: Bool
}

protocol Alertable {
    func presentAlert(title: String?, message: String?, actions: [AlertableAction], completion: ((Bool) -> Void)?)
}

extension Alertable where Self: UIViewController {
    func presentAlert(title: String?, message: String?, actions: [AlertableAction], completion: ((Bool) -> Void)?) {
        let generator = UIImpactFeedbackGenerator(style: .medium)
        generator.impactOccurred()
        let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
        actions.forEach { action in
            alertController.addAction(UIAlertAction(title: action.title, style: action.style, handler: { _ in completion?(action.result) }))
        }
        present(alertController, animated: true, completion: nil)
    }
}

I call this something like

   @objc private func didTapLogout() {
        presentAlert(
            title: nil, message: "Are you sure you want to logout?",
            actions: [
                AlertableAction(title: "No", style: .cancel, result: false),
                AlertableAction(title: "Yes", style: .destructive, result: true),
            ],
            completion: { [weak self] result in
                guard result else { return }
                self?.presenter.logout()
            }
        )
    }

I'd like to write a unit test to assert when this is called, the presented view controller is UIAlertController.

I was trying something like, but it does not pass

    func test_renders_alert_controller() {
        sut.show()

        XCTAssertNotNil(sut.presentedViewController)
    }

    class MockViewController: UIViewController, Alertable {

        var presentViewControllerTarget: UIViewController?

        func show() {
            presentAlert(title: nil, message: "Are you sure you want to logout?", actions:
                [AlertableAction(title: "No", style: .cancel, result: false)],
                completion: nil
            )

            self.presentViewControllerTarget = self.presentedViewController
        }
    }

Solution

  • You need to wait for the UIAlertController to be fully visible before running your assertion.

    Check out XCTWaiter.

    Try something like the below:

        let nav = UINavigationController.init(rootViewController: sut)
    
        sut.show()
    
        let exp = expectation(description: "Test after 1.5 second wait")
        let result = XCTWaiter.wait(for: [exp], timeout: 1.5)
        if result == XCTWaiter.Result.timedOut {
            XCTAssertNotNil(nav.visibleViewController is UIAlertController)
        } else {
            XCTFail("Delay interrupted")
        }