Testing is an important aspect of development. No matters if you belong to the Test Driven Development(TDD) group or not, one should never ignore tests.

Beginning from Xcode 5, Apple introduce the XCTest Framework. Performance measurement was then introduce on Xcode 6 and UI Testing on Xcode 7. However, unit testing a UIViewController is still difficult. This is mainly due to the lifecycle and the way the app module is setup.

Unit test or UITest UIViewController?

Unit test aim to test small portions of our code during development while UI Test tend to only be implemented at the end like a integration test. Also, unit test run a lot faster compare to UI test. I would say, you need both to have a more robust code base.

But unit testing UIViewController is difficult

Yes, is difficult. We have lot of things to take care of before having a suitable test environment. However, difficult does not equate to undoable. We can still write tests or even do TDD for it. These would also help us to understand the lifecycle of UIViewController better.

Usual Set up

This is what I have in most of my UIViewController tests.

import UIKit
import XCTest
@testable import MyPhotoProject

class PhotoViewControllerTests: XCTestCase {

    var viewController: PhotoViewController!

    override func setUp() {
        super.setUp()
        //1
        let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
        let navigationController = storyboard.instantiateInitialViewController() as! UINavigationController
        viewController = navigationController.topViewController as! PhotoViewController

        //2
        UIApplication.shared.keyWindow!.rootViewController = viewController

        //3
        XCTAssertNotNil(navigationController.view)
        XCTAssertNotNil(viewController.view)
    }
}

Number 1 is the setup to create the ViewController that we want to test. If on another app that are using UITabBarController, we can replace the navigationController with it. We can also use instantiateViewController(withIdentifier:) if needed. I usually use a framework called R.swift. It very handy to access viewController, image or identifier that is already in the project.

2 just like what our AppDelegate do, we set it to the rootViewController when the setup is run. This is really important if we are testing views related stuff like UICollectionView.

3 help us to prepare the views with IBOutlet and IBAction connection that is setup via storyboard. I usually use _ = controller.View but have recently change to these after reading it on NatashaTheRobot blog post.

Testing a UICollectionView

So with the above setup, a sample test of UICollectionView in a ViewController is as follows:

func testCollectionViewCellsIsDisplayedWithMatchingImage() {
    //1 Arrange
    let fakeImagesName = ["FakeA", "FakeB", "FakeC"]
    viewController.imagesName = fakeImagesName

    //2 Act
    viewController.collectionView.reloadData()
    RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.5))

    //3 Assert
    let cells = viewController.collectionView.visibleCells as! [PhotoCollectionViewCell]
    XCTAssertEqual(cells.count, fakeImagesName.count, "Cells count should match array.count")
    for I in 0...cells.count - 1 {
        let cell = cells[I]
        XCTAssertEqual(cell.photoImageView.image, UIImage(named: fakeImagesName[I]), "Image should be matching")
    }}

At 1, we Arrange the fake data required for the test.

2, we Act on it. We called the reloadData() to trigger the collectionView delegate. The RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.5)) is required as a "hacky" way of waiting for the data to be loaded.

We then Assert on 3. These is where all the checking is done. Here, we ensure that the count on the cells is correct and each cell.photoImageView.image is equal to the UIImage that it should be displaying.

Quick and Nimble

Quick are a testing framework framework for Swift and Objective-C. Using it together with Nimble, a matcher framework would help us to write better tests and have less boilerplate code.

With the above example, the test would look like this with Quick and Nimble.

class PhotoViewControllerSpec: QuickSpec {
    override func spec() {
        describe("A PhotoViewController") {
            var viewController: PhotoViewController!

            beforeEach {
                let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
                let navigationController = storyboard.instantiateInitialViewController() as! UINavigationController
                viewController = navigationController.topViewController as! PhotoViewController

                let window = UIWindow(frame: UIScreen.main.bounds)
                window.makeKeyAndVisible()
                window.rootViewController = viewController

                _ = viewController.view
            }
            context("should be displaying photo on cells") {
                it("should match datasource count and display correct UIImage") {
                    let fakeImagesName = ["coffee1","coffee2","coffee3","coffee4","coffee5","coffee6"]
                    viewController.imagesName = fakeImagesName
                    viewController.collectionView.reloadData()

                    waitUntil { done in
                        if let cells = viewController.collectionView.visibleCells as? [PhotoCollectionViewCell] {
                            expect(cells).to(haveCount(fakeImagesName.count))
                            for i in 0...cells.count - 1 {
                                let cell = cells[i]
                                expect(cell.photoImageView.image).to(equal(UIImage(named: fakeImagesName[i])))
                            }
                            done()
                        }
                    }
                }
            }
        }
    }
}

Using Quick and Nimble helps us to reduce the Arrange code significantly as the ViewController get larger. It also help us to write better tests with expect instead of XCTAssert.

In the case above, instead of using RunLoop, we can use the waitUntil for the action to complete. We also replace all the XCTAssert with expect. Looking at the expect(cells).to(haveCount(fakeImagesName.count)), it read very much like a English sentence.

Conclusion

Unit testing a UIViewController is certainly doable. As times goes by, Apple would surely improve the XCTest Framework. Covering our code with more tests cases would surely help make the app more robust.