Model-View-Viewmodel And Swift

Testing View Models With Model-View-Viewmodel

Model-View-Viewmodel And Swift

View controllers are notoriously hard to test. Ironically, the Model-View-Controller pattern forces developers to put a lot of the heart and brains of their applications in view controllers.

The Model-View-ViewModel pattern makes testing much easier, another key feature of MVVM. In this tutorial, I'd like to revisit Samsara one more time to show you how easy it is to test the ProfileViewModel class we created earlier in this series.

Setup

Swift has come a long way since it was introduced in 2014 at WWDC. Testing, for example, has become a lot easier. To get started, I created a new file with the Unit Test Case Class template and named it ProfileViewModelTests.

Create Unit Test Case Class

Create Unit Test Case Class

To access the symbols of the Samsara target, we need to add an import statement for the Samsara module. To access declarations that are marked as internal, we prefix the import statement with the @testable attribute.

import XCTest
@testable import Samsara

class ProfileViewModelTests: XCTestCase {

    override func setUp() {
        super.setUp()
    }

    override func tearDown() {
        super.tearDown()
    }

}

I usually use the OCMock library for unit testing, but we can do without it for this example. We are ready to write some unit tests.

Testing the View Model

Xcode's built-in code coverage gives developers a rough idea of how well a code base is covered by unit tests. It isn't perfect, but it helps visualize which code paths are and which ones aren't covered by unit tests.

Let's start with the initializer of the ProfileViewModel class. As a reminder, this is what the initializer of the ProfileViewModel looks like.

init(withProfile profile: Profile) {
    self.profile = profile
}

The test is pretty straightforward. We test that the ProfileViewModel instance isn't nil and that the Profile instance, owned by the view model, is identical to the one we used to initialize the profile view model.

func testInitialization() {
    let profile = Profile()

    // Initialize Profile View Model
    let profileViewModel = ProfileViewModel(withProfile: profile)

    XCTAssertNotNil(profileViewModel, "The profile view model should not be nil.")
    XCTAssertTrue(profileViewModel.profile === profile, "The profile should be equal to the profile that was passed in.")
}

Let's test a few more methods. The next method we test is timeForProfile().

func timeForProfile() -> String {
    return stringFromTimeInterval(profile.duration)
}

In the test, we create a profile, set its duration property to 645.0, initialize a ProfileViewModel instance, and ask it for the formatted time of the profile.

func testTimeForProfile() {
    // Initialize Profile
    let profile = Profile()

    // Configure Profile
    profile.duration = 645.0

    // Initialize Profile View Model
    let profileViewModel = ProfileViewModel(withProfile: profile)

    // Invoke Method to Test
    let timeForProfile = profileViewModel.timeForProfile()

    XCTAssertEqual(timeForProfile, "10:45", "The formatted time should be equal to 10:45.")
}

The implementation of timeForProfile() uses a private helper method, stringFromTimeInterval(_:). This exposes two problems. First, we cannot access a private method in the unit test class. Second, having a private method that formats a Double is a subtle code smell.

It isn't wrong, but it may be better to create an extension on Double and implement it there. The advantage is that we can use stringFromTimeInterval(_:) wherever we want and, as a bonus, it is testable.

To make this happen, I created a new file, Double+Formatting.swift, and declare an extension on Double with one method, toString(). The beauty of this solution is that the functionality to format a Double is bolted onto the Double structure. I really like this technique. This wouldn't be possible in Objective-C.

extension Double {
    func toString() -> String {
        let asInt = Int(self)

        let hours = (asInt / 3600)
        let seconds = (asInt % 60)
        let minutes = ((asInt / 60) % 60)

        if hours  > 0 {
            return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
        } else {
            return String(format: "%02d:%02d", minutes, seconds)
        }
    }
}

We also need to update the ProfileViewModel class. The implementation of timeForProfile() becomes even more elegant.

func timeForProfile() -> String {
    return profile.duration.toString()
}

To make the extension more reusable, you could add methods and parameters to specify the format of the resulting string, but I'm sure you get the idea.

Swift, Testing, and Model-View-ViewModel

My view on Cocoa development has changed significantly since the introduction of Swift and my adoption of the Model-View-ViewModel pattern. Decoupling code, increasing reusability, and improving testability are three key factors that make the projects I work on easier to maintain, more modularized, and less daunting for developers new to the project.

The Model-View-ViewModel pattern has only had benefits for me and, therefore, I don't see a reason why I wouldn't adopt it in future projects. If you haven't given it a try yet, then I encourage you to do so. Start with one view controller to see how it feels. You don't need to spend days or weeks refactoring code to get started with MVVM. Start small.

What's Next?

The Model-View-ViewModel pattern opens up many new avenues and I continue to discover new techniques or patterns as I adopt MVVM in my projects. Questions? Leave them in the comments below or reach out to me on Twitter.

Stop Writing Swift That Sucks

Download the Swift Patterns I Swear By