Group Group Group Group Group Group Group Group Group

Chapter 11, Challenge 1: How to test showAlert

The provided solution does not test the error handling of the CalendarViewController.
It’s kinda misleading that this part was skipped without mentioning that in the book.
Of course, I can understand, if this is something that should be tested with a UI test case.
However, it would’ve been nice to be warned because I certainly wouldn’t have put the effort into finding a solution if I knew beforehand that I’d not be able to compare it.

If I’d go the UI test case route:

  1. How would I provide my error mock to the MockAPI?
  2. How can I use my MockAPI?
  3. How can I get past the login VC or ideally start directly with the calendar VC?

It would be great if there would be more content about UI testing in the book.

What I’ve came up with:

I want to share my solution to testing the UIViewController+showAlert extension.
Maybe someone finds it interesting or can give me feedback on what I’ve came up with:

class CalendarViewController: UIViewController, PresentsErrorViews {
  // …
  var errorViewPresenter: ErrorViewPresenter? = .shared
  // …
}

extension UIViewController {
  /// Show alert; uses `errorViewPresenter` if `self` implements `PresentsErrorViews`;
  /// falls back to the default implementation to allow for incremental adoption.
  func showAlert(
    title: String,
    subtitle: String?,
    type: ErrorViewController.AlertType = .general,
    skin: Skin? = nil
  ) {
    ((self as? PresentsErrorViews)?.errorViewPresenter ?? .shared).present(
      title: title,
      subtitle: subtitle,
      type: type,
      skin: skin
    )
  }
}

class ErrorViewPresenter {
  static let shared = ErrorViewPresenter()

  func present(
    title: String,
    subtitle: String?,
    type: ErrorViewController.AlertType = .general,
    skin: Skin? = nil
  ) {
    // Original `UIViewController+showAlert` implementation
    let alertController = UIStoryboard(name: "Main", bundle: nil)
      .instantiateViewController(withIdentifier: "error") as! ErrorViewController
    alertController.set(title: title, subtitle: subtitle)
    alertController.modalPresentationStyle = .overCurrentContext
    alertController.modalTransitionStyle = .crossDissolve
    alertController.type = type
    alertController.skin = skin
    UIApplication.shared.delegate?.window??.rootViewController?.present(
      alertController,
      animated: true
    )
  }
}

protocol PresentsErrorViews {
  var errorViewPresenter: ErrorViewPresenter? { get set }
}
class ErrorViewPresenterMock: ErrorViewPresenter {
  var presentedErrors = Set<String>()
  
  override func present(
    title: String,
    subtitle: String?,
    type: ErrorViewController.AlertType = .general,
    skin: Skin? = nil
  ) {
    presentedErrors.insert(title)
    super.present(title: title, subtitle: subtitle, type: type, skin: skin)
  }
}

class CalendarViewControllerTests: XCTestCase {
  // …
  var errorViewPresenterMock: ErrorViewPresenterMock!

  override func setUp() {
    // …
    errorViewPresenterMock = ErrorViewPresenterMock()
    sut.errorViewPresenter = errorViewPresenterMock
    // …
  }

  override func tearDown() {
    // …
    errorViewPresenterMock = nil
    // …
  }
  
  func testCalendarViewController_implementsPresentsErrorViews() {
    XCTAssertTrue((sut as AnyObject) is PresentsErrorViews)
  }
  
  func testLoadEvents_whenGetAllFails_presentsError() {
    mockAPI.getEventsErrorMock = mockError()
    let expectedError = "Could not load events"
    
    // when
    let exp = expectation(for: NSPredicate(block: { evpm, _ -> Bool in
      !(evpm as! ErrorViewPresenterMock).presentedErrors.isEmpty
    }), evaluatedWith: errorViewPresenterMock, handler: nil)
    
    sut.loadEvents()
    
    // then
    wait(for: [exp], timeout: 2)
    XCTAssertTrue(errorViewPresenterMock.presentedErrors.contains(expectedError))
  }
}

In Chapter 13 there is a pretty straight forward approach to test these alerts.
See: testSignIn_withBadCredentials_showsError