How I Test View Controllers

By example I want to show how I instantiate and test view controllers from a storyboard in a macOS app using the conventional MVC pattern and AppKit.

This could start with a long line up of different architectures and patterns. Most of them affect the topic of testing differently. Subjectively, for better or worse. And that is what it sometimes comes down to in the developer area in the end: opinions. In the end you should choose what works best for you, your team and your product. After gathering experience across different platforms, layers and technologies the conventional MVC architecture is my first choice for macOS apps built on AppKit. Already because that - for historical reasons - it is how the frameworks are designed. Trying to force another architecture on top of it is trading one bunch of problems against another bunch of problems. Though, one thing that stuck with me from when I picked up SOA (service oriented architecture) while still trying to figure out object-oriented programming long ago is outsourcing services into dedicated types like some PHP web frameworks do. So, I would rather call it MVC+S. The controllers are really just responsible for managing and connecting views to the rest without bloat.

Without a doubt

Preconditions

  • I use only the Main.storyboard in an app project because it usually is sufficient. I am not a big fan of splitting up storyboards or using XIBs. Yikes. Storyboards were invented for a reason and maybe I am part of a minority here: I think they are great.
  • In this example I have a ServerAddressViewController which also is present as a scene in the storyboard.
  • The storyboard identifier of the view controller is set to “ServerAddressViewController” in the main storyboard.

By experience, it is an advantage to have constants in code rather than string literals sprinkled all over the project. It also eases future refactoring, if it is necessary. There is a relation between all occurences of such a reference which can be detected programmatically. To not introduce a new namespace and have a pleasent writing style later, I add an extension to NSStoryboard.SceneIdentifier with static members. One for each identifier from the storyboard. This enables the clean writing style you will see below when instantiating view controllers from the storyboard by identifier.

extension NSStoryboard.SceneIdentifier {
    static let serverAddressViewController: Self = "ServerAddressViewController"
}

Instantiation

Thinking protocol-oriented, how do I start? By defining a protocol. Immediate advantage: it can be mocked. I create a protocol which any object can adopt that is instantiating view controllers. Important detail here: the functions do not return concrete types.

protocol ControllerInstantiating {
    func instantiateServerAddressViewController() -> ServerAddressViewControlling?
}

How to implement this now? Initially I wrote a dedicated type but later moved on to extending the NSStoryboard itself. It already does the instantiation through its related functions anyway.

extension NSStoryboard: ControllerInstantiating {
    func instantiateServerAddressViewController() -> ServerAddressViewControlling? {
        // instantiateController(identifier:) is a generic function which expectes a concrete type.
        // Hence this detour over a stored variable with the concrete type instead of the protocol.
        let controller: ServerAddressViewController = instantiateController(identifier: .serverAddressViewController)
        return controller
    }
}

Note that the ControllerInstantiating protocol and NSStoryboard are not necessary. They are there for convenience, readability in later use and to reduce repetition. Alternatively I would have to write the instantiation code in every test class set up.

This all boils down to this short and convenient setup in test classes:

final class ServerAddressViewControllerTests: XCTestCase {
    var viewController: ServerAddressViewController!

    override func setUpWithError() throws {
        viewController = NSStoryboard.main?.instantiateServerAddressViewController() as? ServerAddressViewController
        _ = viewController.view

        // Dependency injection here
    }

    // Test cases here
}

I do not use this in tests only, by the way. In some cases storyboard segues are either insufficient or I have not figured out a way yet to make them work I want them to. In some situations programmatic instantiation of view controllers is necessary and works fine without messing up the project.

Note the getter call to viewController.view. The result is discarded because it is not needed here. The call triggers the actual loading of the view(s) and setup of storyboard outlets and actions. There also is the loadView() function on NSViewController but as the official documentation states: do not call this yourself.

About The Author

Peter Thomas Horn is a professional software developer at Open-Xchange specialized on the Apple platform. He previously worked a decade across the full stack of various web technologies. Originally started with Java on Windows at the age of 12 years. While staying humble in throwing around buzzwords like "VR" and "machine learning" he occasionally experiences problems and the fitting solutions considered worth sharing.