Unit testing UIViewController life cycle events are not a straightforward process. Let’s have a look on the following view controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import UIKit final class ViewController: UIViewController { var aCollaborator: ACollaboratorProtocol = ACollaborator() override func viewDidLoad() { super.viewDidLoad() aCollaborator.methodA() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) aCollaborator.methodB() } @IBAction func didTapButton(_ sender: Any) { aCollaborator.methodC() showAnotherViewController() } private func showAnotherViewController() { let storyboard = UIStoryboard(name: "Main", bundle: nil) let anotherViewController = storyboard.instantiateViewController(withIdentifier: "AnotherViewController") present(anotherViewController, animated: true, completion: nil) } } |
All we want to test here are the calls to the collaborator and the presentation of a new ViewController as a result of the button action.
In order to test the interactions with the Collaborator we have to replace it with a test double, a spy specifically.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import XCTest @testable import TestingViewControllersLifeCycle class ViewControllerTests: XCTestCase { private final class ACollaboratorMock: ACollaboratorProtocol { private(set) var didCallMethodA = 0 func methodA() { didCallMethodA += 1 } private(set) var didCallMethodB = 0 func methodB() { didCallMethodB += 1 } private(set) var didCallMethodC = 0 func methodC() { didCallMethodC += 1 } } } |
Now let’s start by unit testing the first interaction, the call to methodA when on the viewDidLoad.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class ViewControllerTests: XCTestCase { func test_viewDidLoad_callsMethodAOnCollaborator() { let (sut, mockACollaborator) = makeSUT() _ = sut.view XCTAssertEqual(1, mockACollaborator.didCallMethodA) } private func makeSUT() -> (ViewController, ACollaboratorMock) { let storyboard = UIStoryboard(name: "Main", bundle: nil) let sut = storyboard.instantiateViewController(withIdentifier: "ViewController") as! ViewController let mockACollaborator = ACollaboratorMock() sut.aCollaborator = mockACollaborator return (sut, mockACollaborator) } |
Here the trick is that we need to call sut.view, that is enough to trigger the viewDidLoad method.
Let’s now write the test for the methodB on the viewWillAppear.
1 2 3 4 5 6 7 8 |
func test_viewWillAppear_callsMethodBOnCollaborator() { let (sut, mockACollaborator) = makeSUT() sut.beginAppearanceTransition(true, animated: true) sut.endAppearanceTransition() XCTAssertEqual(1, mockACollaborator.didCallMethodB) } |
This time the calls beginAppearanceTransition and endAppearanceTransition enable us to test the code on the viewWillAppear method.
Last method to test is the didTapButton. We need two tests here one to test the interaction with the Collaborator and one to test that the view is presenting a new view.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
func test_didTapButton_callsMethodCOnCollaborator() { let (sut, mockACollaborator) = makeSUT() sut.didTapButton(UIButton()) XCTAssertEqual(1, mockACollaborator.didCallMethodC) } func test_didTapButton_presentsAnotherViewController() { let (sut, _) = makeSUT() createWindowWith(rootViewController: sut) sut.didTapButton(UIButton()) XCTAssertTrue(sut.presentedViewController is AnotherViewController) } private func createWindowWith(rootViewController viewController: UIViewController) { let window = UIWindow(frame: UIScreen.main.bounds) window.makeKeyAndVisible() window.rootViewController = viewController } |
Right, the first test is easy enough, straightforward case. But checking that the new viewController has been presented requires to add the view controller in a window.
The above tricks will help you to test the majority code in a UIViewController.