If we want to test the AddLocationViewViewModel
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 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.
But how do we stub the responses of the geocoding requests we make? The Core Location framework is a system framework. We cannot mock the CLGeocoder
class. The solution is simple, but it requires a bit of work.
The solution involves three steps:
First, we need to create a service that’s in charge of performing the geocoding requests. That service needs to be injected into the view model. The view model shouldn’t be in charge of 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’s using, that is, as long as 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 AddLocationViewViewModel
class. It shows us what the protocol should look like.
Create a new file in the Protocols group and name it LocationService.swift.
The protocol’s definition will be short. We only define the interface we need to extract the CLGeocoder
class from the view model.
LocationService.swift
We first define a type alias, LocationServiceCompletionHandler
. This is primarily for convenience.
LocationService.swift
More important is the definition of the method that performs the geocoding request, geocode(addressString:completionHandler:)
. It accepts an address string and a completion handler. Because the geocoding request is performed asynchronously, we mark the completion handler as escaping. The completion handler is of type LocationServiceCompletionHandler
.
LocationService.swift
With the LocationService
protocol in place, it’s time to create a class that adopts the LocationService
protocol. Create a new group, Services, and create a class named Geocoder. You can name it whatever you like.
Because we’re going to use the CLGeocoder
class to perform the geocoding requests, we need to import the Core Location framework.
Geocoder.swift
We define the Geocoder
class. The class should conform to the LocationService
protocol we defined earlier.
Geocoder.swift
The Geocoder
class has one private, lazy, variable property, geocoder
, of type CLGeocoder
.
Geocoder.swift
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 AddLocationViewViewModel
class.
Geocoder.swift
This should look familiar. The only difference is that we pass the array of Location
instances to the completion handler of the method along with any errors that pop up.
We now have the ingredients we need to refactor the AddLocationViewController
and AddLocationViewViewModel
classes. Let’s start with the AddLocationViewViewModel
class.
Open AddLocationViewViewModel.swift and replace the geocoder
property with a constant property, locationService
, of type LocationService
.
AddLocationViewViewModel.swift
This simple change means that the AddLocationViewViewModel
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 handy later. It also means we can remove the import statement for the Core Location framework.
AddLocationViewViewModel.swift
We inject a location service into the view model using initializer injection. We pass a second argument to the initializer of the AddLocationViewViewModel
class. The only requirement for the argument is that it conforms to the LocationService
protocol. We set the locationService
property in the initializer.
AddLocationViewViewModel.swift
We also need to update the geocode(addressString:)
method of the AddLocationViewViewModel
class. Because most of the heavy lifting is done by the location service, the implementation is shorter and simpler.
AddLocationViewViewModel.swift
The only 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 line on which we initialize the view model.
AddLocationViewViewModel.swift
That’s it. Build and run the application to make sure we didn’t break anything. The AddLocationViewViewModel
class is now ready to be tested.