In the previous episode, we used the adapter pattern to make the integration of the project with the Google SDK testable. In this episode, we put the theory to the test. We write unit tests for the GoogleAnalyticsService class to validate the integration of the project with the Google SDK.
Creating a Mock Adapter
Before we write unit tests for the GoogleAnalyticsService class, we need to add some structure to the project. Add a group with name Cases to the NotesTests group and move JourneyTests.swift into the Cases group. Add a group with name Mocks to the NotesTests group and move MockAnalyticsService.swift into the Mocks group.
Add a Swift file to the Cases group by choosing the Unit Test Case Class template from the iOS > Source section. Name the class GoogleAnalyticsServiceTests.


Add an import statement for the Notes target and prefix the import statement with the testable attribute. Remove the implementation of the GoogleAnalyticsServiceTests class. We start with a clean slate.
import XCTest
@testable import Notes
internal final class GoogleAnalyticsServiceTests: XCTestCase {
}
Add a unit test with name testAnalyticsServiceConformance(). The idea is simple. We create a GoogleAnalyticsService instance, send an event by invoking the send(event:) method, and verify that the analytics service forwarded the event to its adapter.
If we use a GoogleSDK instance as the adapter of the analytics service, then we won't be able to verify that the analytics service forwarded the event to its adapter and we wouldn't be taking advantage of the changes we made in the previous episode.
We need to create a mock adapter and use it to verify that the analytics service forwarded the event to its adapter, that is, the mock adapter. Add a Swift file to the Mocks group and name it MockGoogleAnalyticsAdapter.swift. Add an import statement for the Notes target and prefix the import statement with the testable attribute. Declare a final class with name MockGoogleAnalyticsAdapter that conforms to the GoogleAnalyticsAdapter protocol.
import Foundation
@testable import Notes
internal final class MockGoogleAnalyticsAdapter: GoogleAnalyticsAdapter {
}
To conform to the GoogleAnalyticsAdapter protocol, the MockGoogleAnalyticsAdapter class needs to implement the trackEvent(with:properties:) method.
import Foundation
@testable import Notes
internal final class MockGoogleAnalyticsAdapter: GoogleAnalyticsAdapter {
// MARK: - Google Analytics Adapter
func trackEvent(with name: String, properties: [String : Any]) {
}
}
We need the ability to verify which events the analytics service forwarded to its adapter. We use the same technique we used for the MockAnalyticsService class. We first declare a nested struct with name Event to encapsulate the details of an event. The Event struct defines two properties, name of type String and properties. The properties property is a dictionary with keys of type String and values of type Any.
import Foundation
@testable import Notes
internal final class MockGoogleAnalyticsAdapter: GoogleAnalyticsAdapter {
// MARK: - Types
struct Event {
// MARK: - Properties
let name: String
let properties: [String: Any]
}
// MARK: - Google Analytics Adapter
func trackEvent(with name: String, properties: [String : Any]) {
}
}
Declare a variable property, events, of type [Event] and set its initial value to an empty array. We declare the setter privately.
import Foundation
@testable import Notes
internal final class MockGoogleAnalyticsAdapter: GoogleAnalyticsAdapter {
// MARK: - Types
struct Event {
// MARK: - Properties
let name: String
let properties: [String: Any]
}
// MARK: - Properties
private(set) var events: [Event] = []
// MARK: - Google Analytics Adapter
func trackEvent(with name: String, properties: [String : Any]) {
}
}
In the trackEvent(with:properties:) method, we use the values of the name and properties parameters to create an Event object and append the Event object to the events array.
// MARK: - Google Analytics Adapter
func trackEvent(with name: String, properties: [String : Any]) {
let event = Event(
name: name,
properties: properties
)
events.append(event)
}
Creating the Analytics Service
With the MockGoogleAnalyticsAdapter class in place, we can create the analytics service for the unit test. Revisit GoogleAnalyticsServiceTests.swift and create a MockGoogleAnalyticsAdapter instance in the testAnalyticsServiceConformance() method. We store a reference to the mock adapter in a constant with name adapter. We use the mock adapter to create a GoogleAnalyticsService instance.
func testAnalyticsServiceConformance() throws {
let adapter = MockGoogleAnalyticsAdapter()
let analyticsService = GoogleAnalyticsService(adapter: adapter)
}
What follows should feel familiar. We create a Journey object and invoke its properties(_:) method to create a Journey.Event object. We pass three properties to the properties(_:) method, kind, source, and wordCount. We send the event to the analytics service by invoking the event's send(to:) method, passing in a reference to the analytics service.
func testAnalyticsServiceConformance() throws {
let adapter = MockGoogleAnalyticsAdapter()
let analyticsService = GoogleAnalyticsService(adapter: adapter)
Journey.createNote
.properties(
.kind(.blank),
.source(.home),
.wordCount(123)
)
.send(to: analyticsService)
}
We access the array of events the mock adapter was asked to send through its events property and assert that the mock adapter received one event.
func testAnalyticsServiceConformance() throws {
let adapter = MockGoogleAnalyticsAdapter()
let analyticsService = GoogleAnalyticsService(adapter: adapter)
Journey.createNote
.properties(
.kind(.blank),
.source(.home),
.wordCount(123)
)
.send(to: analyticsService)
let events = adapter.events
XCTAssertEqual(events.count, 1)
}
What follows is what is most interesting. We store the first event in a constant with name event. We assert that the name of the event is equal to create-note and that the event has three properties.
func testAnalyticsServiceConformance() throws {
let adapter = MockGoogleAnalyticsAdapter()
let analyticsService = GoogleAnalyticsService(adapter: adapter)
Journey.createNote
.properties(
.kind(.blank),
.source(.home),
.wordCount(123)
)
.send(to: analyticsService)
let events = adapter.events
XCTAssertEqual(events.count, 1)
let event = events.first
XCTAssertEqual(event?.name, "create-note")
XCTAssertEqual(event?.properties.count, 3)
}
We also need to verify that the properties we passed to the properties(_:) method earlier are correctly transformed to a dictionary with keys of type String and values of type Any. We use the forEach(_:) method to iterate through the dictionary of properties of the event.
func testAnalyticsServiceConformance() throws {
let adapter = MockGoogleAnalyticsAdapter()
let analyticsService = GoogleAnalyticsService(adapter: adapter)
Journey.createNote
.properties(
.kind(.blank),
.source(.home),
.wordCount(123)
)
.send(to: analyticsService)
let events = adapter.events
XCTAssertEqual(events.count, 1)
let event = events.first
XCTAssertEqual(event?.name, "create-note")
XCTAssertEqual(event?.properties.count, 3)
event?.properties.forEach { name, value in
}
}
In the closure we pass to the forEach(_:) method, we switch on the name of the property. We add a case for each property we expect to be included in the dictionary of properties and assert that the value matches the value we expect. To make the switch statement exhaustive, we add a default case in which we make the unit test fail. If the dictionary of properties contains a property with a name we don't expect, the unit test should fail.
func testAnalyticsServiceConformance() throws {
let adapter = MockGoogleAnalyticsAdapter()
let analyticsService = GoogleAnalyticsService(adapter: adapter)
Journey.createNote
.properties(
.kind(.blank),
.source(.home),
.wordCount(123)
)
.send(to: analyticsService)
let events = adapter.events
XCTAssertEqual(events.count, 1)
let event = events.first
XCTAssertEqual(event?.name, "create-note")
XCTAssertEqual(event?.properties.count, 3)
event?.properties.forEach { name, value in
switch name {
case "kind":
XCTAssertEqual(value as? String, "blank")
case "source":
XCTAssertEqual(value as? String, "home")
case "word_count":
XCTAssertEqual(value as? Int, 123)
default:
XCTFail("Invalid Property")
}
}
}
Run the test suite to verify that the unit test for the GoogleAnalyticsService class passes. Open the Report Navigator on the left and select the Coverage report. It confirms that GoogleAnalyticsService.swift is covered by the test suite.

What's Next?
Even though the adapter pattern adds complexity to the implementation, I hope it is clear that that complexity has an important benefit. It makes the integration of the project with the Google SDK testable.