In the previous episode, we made the Core Location framework a dependency of the project. That is fine, but we don't want a dependency to compromise the testability of the project. In this episode, we use a proven and familiar pattern to improve the testability of the GeocodingClient class.
It Starts with a Protocol
You may already know which solution I have in mind. It starts with a protocol. Add a Swift file to the Geocoding group and name it Geocoder.swift. Add an import statement for the Core Location framework and declare a protocol with name Geocoder.
import CoreLocation
protocol Geocoder {
}
The protocol defines one method, geocodeAddressString(_:). The method should look familiar because its signature is identical to that of the CLGeocoder class. It accepts an address, a string, as its only argument. It is asynchronous, throwing, and returns an array of CLPlacemark instances.
import CoreLocation
protocol Geocoder {
func geocodeAddressString(_ addressString: String) async throws -> [CLPlacemark]
}
Updating the Geocoding Client
Open GeocodingClient.swift. The geocoder property of the GeocodingClient class is no longer of type CLGeocoder. We change its type to Geocoder, the protocol we defined a moment a ago. The Geocoder object should be injected into the geocoding client to improve its testability. We don't create a CLGeocoder instance and store a reference to it in the geocoder property.
import CoreLocation
final class GeocodingClient: GeocodingService {
...
// MARK: - Properties
private let geocoder: Geocoder
...
}
We inject the Geocoder object through initializer injection. Define an initializer that accepts an object of type Geocoder. In the body of the initializer, we store a reference to the Geocoder object in the geocoder property.
// MARK: - Initialization
init(geocoder: Geocoder) {
self.geocoder = geocoder
}
The application should still use a CLGeocoder instance to forward geocode addresses. It is only when running the test suite that we inject a mock that conforms to the Geocoder protocol. For that reason, we define a default value for the geocoder parameter. That default value is a CLGeocoder instance.
// MARK: - Initialization
init(geocoder: Geocoder = CLGeocoder()) {
self.geocoder = geocoder
}
Conforming CLGeocoder to Geocoder
The compiler throws an error because the CLGeocoder class doesn't conform to the Geocoder protocol. Let's resolve the error with an extension for CLGeocoder. Add a group with name Extensions. Add a Swift file to the group and name it CLGeocoder+Geocoder.swift.
Add an import statement for the Core Location framework and create an extension for the CLGeocoder class. We use the extension to conform the CLGeocoder class to the Geocoder protocol. That's it. We don't need to implement the geocodeAddressString(_:) method because the CLGeocoder class already defines a method with that name.
import CoreLocation
extension CLGeocoder: Geocoder {
}
Build the Thunderstorm target to make sure that we didn't break anything.
What's Next?
It is important to decouple dependencies from the project as much as possible. Not only does that improve the testability of the project, decoupled dependencies are much easier to remove or replace.