In the previous episodes, we unit tested the analytics library we built. The integration with the Google SDK is an aspect of the implementation we haven't talked about in detail. We shouldn't unit test the Google SDK itself, but can we unit test the integration with the project? That is the question we focus on in this episode.
Avoiding Confusion
To avoid confusion in this episode, we rename the GoogleAnalyticsClient class to GoogleSDK. I want to make it clear in this example that the GoogleSDK class is a third party library we integrate with and have no control over.
Open GoogleAnalyticsClient.swift and rename the GoogleAnalyticsClient class to GoogleSDK. We also rename the comment at the top and the file itself.
//
// GoogleSDK.swift
// Notes
//
// Copyright Cocoacasts
// Created by Bart Jacobs
//
import Foundation
internal final class GoogleSDK {
...
}
Open GoogleAnalyticsClient+AnalyticsService.swift and update the references to GoogleAnalyticsClient to GoogleSDK, including the name of the file.
//
// GoogleSDK+AnalyticsService.swift
// Notes
//
// Copyright Cocoacasts
// Created by Bart Jacobs
//
extension GoogleSDK: AnalyticsService {
...
}
Open AppDelegate.swift and navigate to the setupDependencies() method. Update the reference to GoogleAnalyticsClient to GoogleSDK.
// MARK: - Helper Methods
private func setupDependencies() {
Container.shared.register(AnalyticsService.self) { _ in
GoogleSDK(
apiKey: Configuration.Google.apiKey,
clientSecret: Configuration.Google.clientSecret
)
}
}
Run the test suite to make sure the target builds and the test suite passes.
Exploring the Limitations of Unit Tests
The GoogleSDK class is part of the Google SDK so we won't be writing unit tests for that class. How the Google SDK is integrated into the project is something we should consider unit testing. The code that integrates the Google SDK and the project is located in the extension we created for the GoogleSDK class.
Revisit GoogleSDK+AnalyticsService.swift. Earlier in this series, we created an extension for the GoogleSDK class to conform it to the AnalyticsService protocol. In the send(event:) method, the Journey.Event object is transformed in such a way that it can be passed to the trackEvent(with:properties:) method of the GoogleSDK class.
extension GoogleSDK: AnalyticsService {
func send(event: Journey.Event) {
var properties: [String: Any] = [:]
event.properties.forEach { property in
properties[property.name] = property.value
}
trackEvent(with: event.name, properties: properties)
}
}
It isn't possible to write a unit test for the send(event:) method of the GoogleSDK class because we don't have access to the internals of the GoogleSDK class. We can unit test the integration to some extent by taking a protocol-oriented approach in which we remove the Google SDK from the equation.
Applying the Adapter Pattern
The solution I have in mind leverages the adapter pattern. We first create a type that conforms to the AnalyticsService protocol. The GoogleSDK class should no longer act as the analytics service. Add a group with name Services. Add a Swift file to the Services group and name it GoogleAnalyticsService.swift.
Declare a final class with name GoogleAnalyticsService that conforms to the AnalyticsService protocol. To conform to the AnalyticsService protocol, the GoogleAnalyticsService class needs to implement the send(event:) method.
import Foundation
internal final class GoogleAnalyticsService: AnalyticsService {
// MARK: - Analytics Service
func send(event: Journey.Event) {
}
}
Open GoogleSDK+AnalyticsService.swift in the assistant editor on the right. We need to make two changes to the extension for the GoogleSDK class. First, the GoogleSDK class should no longer conform to the AnalyticsService protocol. Second, move the implementation of the send(event:) method from the extension to the GoogleAnalyticsService class.
import Foundation
internal final class GoogleAnalyticsService: AnalyticsService {
// MARK: - Analytics Service
func send(event: Journey.Event) {
var properties: [String: Any] = [:]
event.properties.forEach { property in
properties[property.name] = property.value
}
trackEvent(with: event.name, properties: properties)
}
}
We end up with an empty extension for the GoogleSDK class and a compiler error in the GoogleAnalyticsService class.
extension GoogleSDK {
}
The GoogleAnalyticsService class invokes a method with name trackEvent(with:properties:), but it doesn't have such a method. That is a method of the Google SDK. This is where the adapter pattern comes into play.
Add a Swift file to the Protocols group and name it GoogleAnalyticsAdapter.swift. Declare a protocol with name GoogleAnalyticsAdapter. The protocol declares one method, trackEvent(with:properties:). The method accepts the name of the event, a string, and the properties of the event, a dictionary.
import Foundation
protocol GoogleAnalyticsAdapter {
func trackEvent(with name: String, properties: [String: Any])
}
Let's now revisit the implementation of the GoogleAnalyticsService class. Declare a private, constant property, adapter, of type GoogleAnalyticsAdapter.
import Foundation
internal final class GoogleAnalyticsService: AnalyticsService {
// MARK: - Properties
private let adapter: GoogleAnalyticsAdapter
...
}
We also implement an initializer that accepts a GoogleAnalyticsAdapter object. The initializer stores a reference to the adapter in the adapter property.
import Foundation
internal final class GoogleAnalyticsService: AnalyticsService {
// MARK: - Properties
private let adapter: GoogleAnalyticsAdapter
// MARK: - Initialization
init(adapter: GoogleAnalyticsAdapter) {
self.adapter = adapter
}
...
}
With these changes in place, the GoogleAnalyticsService class now has access to an object that defines a trackEvent(with:properties:) method and we use the adapter in the send(event:) method to send the event.
// MARK: - Analytics Service
func send(event: Journey.Event) {
var properties: [String: Any] = [:]
event.properties.forEach { property in
properties[property.name] = property.value
}
adapter.trackEvent(with: event.name, properties: properties)
}
Integrating the Google SDK
The remainder of the implementation is simple. Open GoogleSDK+AnalyticsService.swift and use the extension for the GoogleSDK class to conform it to the GoogleAnalyticsAdapter protocol. We don't need to implement the trackEvent(with:properties:) method because the GoogleSDK class already defines a method with that name.
extension GoogleSDK: GoogleAnalyticsAdapter {
}
To avoid confusion, rename GoogleSDK+AnalyticsService.swift to GoogleSDK+GoogleAnalyticsAdapter.swift.
The last piece of the puzzle is updating the setupDependencies() method in AppDelegate.swift. The GoogleSDK class no longer conforms to the AnalyticsService protocol so we cannot register it with the dependency injection container. We store a reference to the GoogleSDK instance in a constant with name adapter because that is what the GoogleSDK instance is, an adapter. We create a GoogleAnalyticsService instance, passing the adapter to the initializer, and return the GoogleAnalyticsService instance from the closure.
private func setupDependencies() {
Container.shared.register(AnalyticsService.self) { _ in
let adapter = GoogleSDK(
apiKey: Configuration.Google.apiKey,
clientSecret: Configuration.Google.clientSecret
)
return GoogleAnalyticsService(adapter: adapter)
}
}
Run the test suite to make sure the target builds and the test suite passes.
What Did We Gain?
You may be wondering what we gained with this added complexity. That is a fair question because it may not be immediately obvious. By applying the adapter pattern, we have more control over the integration with the Google SDK.
If the GoogleSDK class conforms to the AnalyticsService protocol, then the GoogleSDK class transform the Journey.Event object and sends it to Google. That transformation is a critical piece of the integration and it is something we cannot unit test.
By making use of an adapter, that transformation is handled by the GoogleAnalyticsService class. The GoogleAnalyticsService class passes the name and properties of the event to its adapter. Because the transformation is handled by a type we control, we can more easily unit test that critical piece of the integration.
What's Next?
In the next episode, we write unit tests for the GoogleAnalyticsService class and that should clear up any confusion. It is important to note that the changes we made in this episode are optional. The implementation we started with works fine and it is decoupled from the Google SDK. As I mentioned earlier in this series, it is up to you to decide what aspects of the project you want to cover with unit tests.