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.
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.
Mastering MVVM With Swift
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.
If you are serious about MVVM or you are tired of MVC, check out Mastering MVVM With Swift. We transform an application that uses MVC to use MVVM instead. You learn everything you need to know to use MVVM in your own projects. Did I mention we also cover RxSwift and Combine?