If we want to unit test the AddLocationViewModel class, we need the ability to stub the responses of the geocoding requests we make to Apple's location services. Only then can we write fast and reliable unit tests. Being in control of your environment is essential if your goal is creating a fast and robust test suite.
Not only do we want to be in control of the response we receive from the geocoding requests, we don't want the test suite to rely on a service we don't control. It can make the test suite slow and unreliable.
How do we stub the responses of the geocoding requests the application makes? The Core Location framework is a system framework. We cannot mock the CLGeocoder class. The solution isn't difficult, but it requires a bit of work.
A Plan of Action
The solution involves three steps:
First, we need to create a service in charge of performing the geocoding requests. That service is injected into the view model because the view model shouldn't be responsible for instantiating the service.
Second, the service we inject into the view model conforms to a protocol we define. The protocol is nothing more than a definition of an interface that allows the view model to initiate a geocoding request. It initiates the geocoding request, it doesn't perform the geocoding request.
Third, the service conforms to the protocol and we inject an instance of the service into the view model.
Not only does this solution decouple the view model from the Core Location framework, the view model won't even know which service it is using. The view model only cares that the service conforms to the protocol we define.
Don't worry if this sounds confusing. Let's start by creating the protocol for the service. We can draw inspiration from the current implementation of the AddLocationViewModel class. It shows us exactly what the protocol should look like.
Defining the Protocol
Create a new file in the Protocols group and name it LocationService.swift.

We can keep the protocol's definition short. We only define the interface we need to extract the CLGeocoder class from the view model.
LocationService.swift
protocol LocationService {
}
We first define a type alias with name, Completion. The type of the type alias is a closure that accept an argument of type Result and returns Void. The associated value of the success case of the Result type is of type [Location]. The associated value of the failure case of the Result type is of type LocationServiceError.
LocationService.swift
protocol LocationService {
// MARK: - Type Aliases
typealias Completion = (Result<[Location], LocationServiceError>) -> Void
}
The next step is defining the LocationServiceError enum. Because types cannot be nested within the a protocol definition, we define the LocationServiceError enum below the LocationService protocol. The enum conforms to the Error protocol and defines two cases, invalidAddressString and requestFailed. The requestFailed case has an associated value of type Error. Why that is becomes clear in a moment.
protocol LocationService {
// MARK: - Type Aliases
typealias Completion = (Result<[Location], LocationServiceError>) -> Void
}
// MARK: - Types
enum LocationServiceError: Error {
// MARK: - Cases
case invalidAddressString
case requestFailed(Error)
}
The LocationService protocol defines a single method. The method performs the geocoding request and is named geocode(addressString:completion:). The method accepts an address string and a closure. Because the geocoding request is performed asynchronously, we mark the closure as escaping by annotating it with the escaping attribute.
LocationService.swift
protocol LocationService {
// MARK: - Type Aliases
typealias Completion = (Result<[Location], LocationServiceError>) -> Void
// MARK: - Methods
func geocode(addressString: String, completion: @escaping Completion)
}
// MARK: - Types
enum LocationServiceError: Error {
// MARK: - Cases
case invalidAddressString
case requestFailed(Error)
}
Adopting the Protocol
With the LocationService protocol in place, it is time to create a class that adopts the LocationService protocol. Create a new group, Services, and create a file with name Geocoder.swift.

Because we use the CLGeocoder class to perform the geocoding requests, we need to import the Core Location framework at the top.
Geocoder.swift
import CoreLocation
We define a class with name Geocoder that conforms to the LocationService protocol we defined earlier.
Geocoder.swift
import CoreLocation
class Geocoder: LocationService {
}
The Geocoder class has one private, lazy, variable property, geocoder, of type CLGeocoder.
Geocoder.swift
import CoreLocation
class Geocoder: LocationService {
// MARK: - Properties
private lazy var geocoder = CLGeocoder()
}
The only thing left to do is implement the method of the LocationService protocol. This isn't difficult since most of the implementation can be found in the current implementation of the AddLocationViewModel class.
Geocoder.swift
import CoreLocation
class Geocoder: LocationService {
// MARK: - Properties
private lazy var geocoder = CLGeocoder()
// MARK: - Location Service
func geocode(addressString: String, completion: @escaping Completion) {
guard !addressString.isEmpty else {
completion(.failure(.invalidAddressString))
return
}
// Geocode Address String
geocoder.geocodeAddressString(addressString) { (placemarks, error) in
if let error = error {
completion(.failure(.requestFailed(error)))
} else if let placemarks = placemarks {
// Create Locations
let locations = placemarks.compactMap({ (placemark) -> Location? in
guard let name = placemark.name else { return nil }
guard let location = placemark.location else { return nil }
return Location(name: name, latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
})
completion(.success(locations))
}
}
}
}
This should look familiar. The implementation is clear and elegant thanks to the use of the Result type. The Result type makes it possible to convert an inelegant Objective-C API into an elegant Swift API.
We now have the ingredients we need to refactor the AddLocationViewController and AddLocationViewModel classes. Let's start with the AddLocationViewModel class.
Refactoring the Add Location View Model
Open AddLocationViewModel.swift and replace the geocoder property with a constant property, locationService, of type LocationService.
AddLocationViewModel.swift
// MARK: -
private let locationService: LocationService
This simple change means that the AddLocationViewModel class no longer knows how the application performs the geocoding requests. It could be the Core Location framework, but it might as well be some other library. This will come in useful later. It also means we can remove the import statement for the Core Location framework at the top.
AddLocationViewModel.swift
import Combine
import Foundation
class AddLocationViewModel {
...
}
We inject a location service into the view model using initializer injection. We pass an argument to the initializer of the AddLocationViewModel class. The only requirement for the argument is that it conforms to the LocationService protocol. We set the locationService property in the initializer.
AddLocationViewModel.swift
// MARK: - Initialization
init(locationService: LocationService) {
// Set Properties
self.locationService = locationService
// Setup Bindings
setupBindings()
}
We also need to update the geocode(addressString:) method of the AddLocationViewModel class. Because most of the heavy lifting is handled by the location service, the implementation is shorter and simpler.
AddLocationViewModel.swift
// MARK: - Helper Methods
private func geocode(addressString: String?) {
guard let addressString = addressString else {
// Reset Locations
locations = []
return
}
// Update Helper
querying = true
// Geocode Address String
locationService.geocode(addressString: addressString) { [weak self] (result) in
// Update Helper
self?.querying = false
switch result {
case .success(let locations):
self?.locations = locations
case .failure(let error):
self?.locations = []
print("Unable to Forward Geocode Address (\(error)")
}
}
}
Refactoring the Add Location View Controller
The change we need to make in the AddLocationViewController class is small. Open AddLocationViewController.swift and navigate to the viewDidLoad() method. We only need to update the initialization of the view model in the viewDidLoad() method. The view controller creates a Geocoder instance and passes it to the initializer of the AddLocationViewModel class.
AddLocationViewModel.swift
// Initialize View Model
viewModel = AddLocationViewModel(locationService: Geocoder())
That's it. Build and run the application to make sure we didn't break anything. The AddLocationViewModel class is now ready to be unit tested.