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:
- How would I provide my error mock to the
MockAPI
? - How can I use my
MockAPI
? - 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))
}
}