This is the third and last installment of a series that teaches you how to mock and stub system classes in Swift. In this tutorial, we unit test the AddLocationViewViewModel class.

We create a new unit test case class, name it AddLocationViewViewModelTests, and remove the sample tests Xcode added for us.

Create a Unit Test Case Class

Create a Unit Test Case Class

Because the AddLocationViewViewModel class uses RxSwift and RxCocoa, we need to add an import statement for RxSwift. We also add import statements for RxTest and RxBlocking. These libraries are part of the RxSwift project and make testing reactive code much easier. It's one of the best features of the RxSwift project.

Last but not least, we import the Cloudy module to make sure we have access to the AddLocationViewViewModel class. By prefixing the import statement with the testable attribute, internal methods are also accessible from within the test target.

import XCTest
import RxTest
import RxSwift
import RxBlocking

@testable import Cloudy

class AddLocationViewViewModelTests: XCTestCase {

    // MARK: - Set Up & Tear Down

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

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

}

Before we can write any tests, we need to declare a few properties:

  • viewModel of type AddLocationViewViewModel!
  • scheduler of type SchedulerType!
  • query of type Variable<String>!

Notice that these properties are implicitly unwrapped optionals. Safety isn't a major concern when I write unit tests. It's more important for the unit tests to be reliable and easy to read. If something goes wrong, it means I made a mistake in one of the test cases.

The viewModel property is the view model we'll be testing. The scheduler property is less important for our discussion. Schedulers provide a layer of abstraction for scheduling operations using RxSwift. That's all you need to know about schedulers to follow along.

We use the query property to mock user input. Because the view model doesn't know where the input comes from, it's easy to mock user input by emitting events through the query variable.

// MARK: - Properties

var viewModel: AddLocationViewViewModel!

// MARK: -

var scheduler: SchedulerType!

// MARK: -

var query: Variable<String>!

We prepare for every unit test in the setUp() method of the test case. Remember that this method is executed every time a test is run. It ensures we start with a clean slate. In the setUp() method, we create a Variable of type String and assign it to the query property. The variable is initialized with an empty string.

override func setUp() {
    super.setUp()

    // Initialize Query
    query = Variable<String>("")
}

The next step is initializing the view model. We have a problem, though. If we use the Geocoder class we implemented in the previous tutorial, we cannot stub the response of the geocoding request. Remember that we want to control the environment in which the test suite is run. If the application talks to a location service, we need the ability to control its response.

Fortunately, we already did the heavy lifting to make this very easy. All we need to do is create a mock location service. We start by declaring a private, nested class, MockLocationService, which conforms to the LocationService protocol.

private class MockLocationService: LocationService {

}

The only method we need to implement to conform to the LocationService protocol is geocode(addressString:completionHandler:). If addressString has a value and its value isn't an empty string, we invoke the completion handler with an array containing one location. The second argument of the completion handler, an optional error, is nil. Notice that we control what the location service returns. In this example, we return a Location instance with a name of Brussels and a fixed set of coordinates.

private class MockLocationService: LocationService {

    func geocode(addressString: String?, completionHandler: @escaping LocationServiceCompletionHandler) {
        if let addressString = addressString, !addressString.isEmpty {
            // Create Location
            let location = Location(name: "Brussels", latitude: 50.8503, longitude: 4.3517)

            // Invoke Completion Handler
            completionHandler([location], nil)
        } else {
            // Invoke Completion Handler
            completionHandler([], nil)
        }
    }

}

If addressString has no value or its value is equal to an empty string, we invoke the completion handler with an empty array. The second argument of the completion handler, an optional error, is nil.

That's it. In the setUp() method, we can now instantiate an instance of the MockLocationService class and pass it as an argument to the initializer of the AddLocationViewViewModel class. The first argument of the initializer is the query property passed in as a driver.

override func setUp() {
    super.setUp()

    // Initialize Query
    query = Variable<String>("")

    // Initialize Location Service
    let locationService = MockLocationService()

    // Initialize View Model
    viewModel = AddLocationViewViewModel(query: query.asDriver(), locationService: locationService)
}

We also create a concurrent dispatch queue scheduler and assign it to the scheduler property. Don't worry about this if you're not familiar with RxSwift.

override func setUp() {
    super.setUp()

    // Initialize Query
    query = Variable<String>("")

    // Initialize Location Service
    let locationService = MockLocationService()

    // Initialize View Model
    viewModel = AddLocationViewViewModel(query: query.asDriver(), locationService: locationService)

    // Initialize Scheduler
    scheduler = ConcurrentDispatchQueueScheduler(qos: .default)
}

To prove that the mock location service does the job we want it to do, I'm going to write a unit test that tests the locations driver of the view model. Now that we control what the location service returns, we can test the behavior of the view model. We name the test testLocations_HasLocations(). We create an observable and store a reference to it in a constant named observable.

// Create Subscription
let observable = viewModel.locations.asObservable().subscribeOn(scheduler)

We emit a new event by setting the value of the query property. This mimics the user entering a city name in the search bar of the add location view controller.

// Set Query
query.value = "Brus"

We use toBlocking() to make the test synchronous. The result, an array of Location instances, is stored in the result constant.

// Fetch Result
let result = try! observable.skip(1).toBlocking().first()!

We assert that result isn't equal to nil and that it contains one element.

XCTAssertNotNil(result)
XCTAssertEqual(result.count, 1)

We also include an assertion for the location stored in the array by making sure the name property of the location is equal to Brussels.

// Fetch Location
let location = result.first!

XCTAssertEqual(location.name, "Brussels")

Notice that we make ample use of the exclamation mark. If anything blows up, it's because of a failed test or an error we made. In other words, we made a mistake that we need to fix. This is what the unit test looks like.

func testLocations_HasLocations() {
    // Create Subscription
    let observable = viewModel.locations.asObservable().subscribeOn(scheduler)

    // Set Query
    query.value = "Brus"

    // Fetch Result
    let result = try! observable.skip(1).toBlocking().first()!

    XCTAssertNotNil(result)
    XCTAssertEqual(result.count, 1)

    // Fetch Location
    let location = result.first!

    XCTAssertEqual(location.name, "Brussels")
}

What Have We Accomplished

If you're new to dependency injection and protocol-oriented programming, it may seem a lot of work to mock the Core Location framework. That may be true because we had to start from scratch. We extracted the CLGeocoder class from the AddLocationViewViewModel class, we defined the LocationService protocol, we implemented the Geocoder class, and we injected a Geocoder instance into the AddLocationViewViewModel class.

That's a lot of work. Isn't it? But if you embrace protocol-oriented programming in the code you write day to day, that's how you architect your projects. You make sure that the code you write is decoupled and easy to test. That's the default. Protocol-oriented programming and dependency injection allow you to do that.

I hope you agree that the result is pretty nice. The AddLocationViewViewModel class has no clue what type of object performs the geocoding requests. The only requirement the AddLocationViewViewModel class sets is that the object conforms to the LocationService protocol. This makes it easy to inject a mock location service when we're testing. We can even replace the location service with a third party service if necessary.

Even though mocking and stubbing is easier if you can take advantage of the powerful Objective-C runtime, I like how Swift forces me to rethink the code I write. This is something I've written about in the past. By writing unit tests, you automatically write better code. Give it a try.