Test View Controller Push to Navigation Controller

There are different ways to test that the UIViewController has been successfully pushed onto the navigation stack and in this tutorial, I will cover a few of them.

For this tutorial, I have created a simple project with two view controllers on the Main.storyboard. The very first view controller has a button. When the button is tapped, it takes a user to the second view controller by pushing it into a navigation stack.

If you are interested in video lessons on how to write Unit tests and UI tests to test your Swift mobile app, check out this page: Unit Testing Swift Mobile App

Unit Test Push to Navigation Controller in Swift

The implementation of the first view controller is very basic. It only handles a button tap event.

import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var nextViewButton: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    @IBAction func nextViewButtonTapped(_ sender: Any) {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let secondViewController = storyboard.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
    
        self.navigationController?.pushViewController(secondViewController, animated: true)
    }
    
}

The Test Case Class

To unit test that the view controller is pushed to a navigation controller, I have first created the Unit Test Case class that loads the first ViewController class from the main storyboard. To learn about different ways you can load a UIViewController into test case class, have a look at this tutorial: Ways to load a UIViewController in a Unit Test.

import XCTest
@testable import TestNavigationTutorial

class TestNavigationTutorialTests: XCTestCase {
    
    var sut: ViewController!
    var navigationController: UINavigationController!
    
    override func setUpWithError() throws {
        // Step 1. Create an instance of UIStoryboard
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        
        // Step 2. Instantiate UIViewController with Storyboard ID
        sut = storyboard.instantiateViewController(withIdentifier: "ViewController") as? ViewController
        
        // Step 3. Make the viewDidLoad() execute.
        sut.loadViewIfNeeded()
        navigationController = UINavigationController(rootViewController: sut)
    }
    
    override func tearDownWithError() throws {
        sut = nil
        navigationController = nil
    }
 
}

Test Navigation with Predicate and Expectation

In the code snippet below I use the Predicate and an Expectation to test if the second view controller has been successfully pushed into a navigation controller.

Although it works well, I do not like this approach much because it makes my unit test wait for almost 2 seconds for an expectation to successfully fulfill. This is because when the button is tapped, due to animation, it takes a little time for a second view controller to be pushed into the navigation stack. This does not take place immediately. The wait time of 2 seconds in my opinion is too long. If I have to test the navigation between 20 view controllers then the wait time only is about 40 seconds. I would better mock the navigation controller then.

func testNextViewButton_WhenTapped_SecondViewControllerIsPushed() throws {
    
    // Create a Predicate to evaluate if the TopViewController is a SecondViewController
    let myPredicate = NSPredicate { input, _ in
        return (input as? UINavigationController)?.topViewController is SecondViewController
    }
    
    // Create Expectation for a Predicate.
    // This will make our unit test method, continously evalulate the
    // predicate until it matches or expectation times out.
    self.expectation(for: myPredicate, evaluatedWith: navigationController)
    
    // Act
    sut.nextViewButton.sendActions(for: .touchUpInside)
    
    waitForExpectations(timeout: 2)
}

Test Navigation –  a better approach

The below code snippet does not wait for a predicate condition to successfully evaluate and simply runs the RunLoop one more time. It works much faster.

In the code snippet below if the topViewController is not the SecondViewController, then the XCTFail() will fail the test method. Otherwise, the execution will continue and the test method will simply pass.

func testNextViewButton_WhenTapped_SecondViewControllerIsPushed2() throws {
    
    // Act
    sut.nextViewButton.sendActions(for: .touchUpInside)
    RunLoop.current.run(until: Date())
    
    // Assert
    guard let _ = navigationController.topViewController as? SecondViewController else {
        XCTFail()
        return
    }
    
}

Test Navigation. An approach with a Spy object

Another way to learn if the view controller was pushed into a navigation controller is to spy on the pushViewController() method. This way you can avoid any waiting time and not even need to run the RunLoop. Although I still like the approach with the RunLoop as it seems to be simpler and always works.

Creating a Spy Class for UINavigationController

import UIKit
 
class SpyNavigationController: UINavigationController {
    
    var pushedViewController: UIViewController?
    
    override func pushViewController(_ viewController: UIViewController, animated: Bool) {
        pushedViewController = viewController
        super.pushViewController(viewController, animated: true)
    }
}

Unit Test Method

func testNextViewButton_WhenTapped_SecondViewControllerIsPushed3() throws {
    // Wrap ViewController into a Spy version of Navigation Controller
    let mockNavigationController = SpyNavigationController(rootViewController: sut)

    // Act
    sut.nextViewButton.sendActions(for: .touchUpInside)
 
    // Assert
    guard let _ = mockNavigationController.pushedViewController as? SecondViewController else {
        XCTFail()
        return
    }
    
}

I hope this short tutorial was helpful to you. If you are interested to learn more about Unit testing and UI Testing of your Swift mobile application, then have a look at my video lessons here – “Unit Testing Swift Mobile App“.

Happy Unit Testing! 🙋🏻‍♂️

Leave a Reply

Your email address will not be published. Required fields are marked *