In the previous episodes, we used code coverage to help us write unit tests for the analytics library we built. This worked fine, but code coverage isn't perfect. Code coverage inspects which lines of code are executed when the test suite is run. The result may be misleading or incorrect at times. In this episode, we take a look at an important shortcoming of code coverage.
Finding Gaps in the Test Suite
You should never blindly trust the tools you use and code coverage is no exception. Open Journey.swift and inspect the code coverage gutter on the right. It shows that the Journey enum, the Event struct, and the Property enum are covered by the unit tests we wrote in the previous episodes.
Run the test suite. Open the Report Navigator on the left and select the Coverage report. It confirms that Journey.swift is covered by the test suite. This isn't accurate, though. Let me explain why that is.

Navigate to the properties(_:) method of the Journey enum. In the body of the method, we check if the collection of properties is empty. If it is empty, then we create and return an Event object by passing an empty array to the initializer. If it isn't empty, then we create and return an Event object by passing the collection of properties to the initializer. Even though the implementation is slightly different, the behavior of the properties(_:) method remains unchanged.
func properties(_ properties: Property...) -> Event {
if properties.isEmpty {
return Event(name: event, properties: [])
}
return Event(name: event, properties: properties)
}
Run the test suite. This time the code coverage gutter shows a bit of red and the code coverage report confirms that the Journey enum isn't entirely covered by the test suite.

Open JourneyTests.swift. In the testEventNames() method, we create an Event object by invoking the properties(_:) method. We don't pass properties to the properties(_:) method and that is why the updated implementation of the properties(_:) method isn't entirely covered by the testEventNames() method.
To fill the gap in the test suite, we need to write a unit test that asserts that the collection of properties that are passed to the properties(_:) method are equal to the properties property of the Event object the properties(_:) method returns. Let's write that unit test to fill the gap in the test suite.
Filling the Gap in the Test Suite
Add a unit test with name testEventProperties(). The implementation is similar to that of the testEventNames() method. We create a MockAnalyticsService instance to send and capture events.
// MARK: - Tests for Event Properties
func testEventProperties() throws {
let analyticsService = MockAnalyticsService()
}
We create a Journey object and invoke the properties(_:) method to create an Event object. We pass three properties to the properties(_:) method. We send the event to the mock analytics service by invoking the event's send(to:) method, passing in a reference to the mock analytics service. This should look familiar.
func testEventProperties() throws {
let analyticsService = MockAnalyticsService()
Journey.createNote
.properties(
.kind(.blank),
.source(.home),
.wordCount(123)
)
.send(to: analyticsService)
}
We access the array of events the mock analytics service was asked to send through its events property and assert that the mock analytics service received one event.
func testEventProperties() throws {
let analyticsService = MockAnalyticsService()
Journey.createNote
.properties(
.kind(.blank),
.source(.home),
.wordCount(123)
)
.send(to: analyticsService)
let events = analyticsService.events
XCTAssertEqual(events.count, 1)
}
We store the properties of the first event in a constant, properties, and assert that the first event has three properties.
func testEventProperties() throws {
let analyticsService = MockAnalyticsService()
Journey.createNote
.properties(
.kind(.blank),
.source(.home),
.wordCount(123)
)
.send(to: analyticsService)
let events = analyticsService.events
XCTAssertEqual(events.count, 1)
let properties = events.first?.properties
XCTAssertEqual(properties?.count, 3)
}
We could stop here, but let's take it one step further. We assert that the properties of the first event are equal to the properties we passed to the properties(_:) method. This requires a bit of code duplication because the first parameter of the properties(_:) method is a variadic parameter. It isn't possible to pass an array of properties to the properties(_:) method.
func testEventProperties() throws {
let analyticsService = MockAnalyticsService()
Journey.createNote
.properties(
.kind(.blank),
.source(.home),
.wordCount(123)
)
.send(to: analyticsService)
let events = analyticsService.events
XCTAssertEqual(events.count, 1)
XCTAssertEqual(events.first?.properties, [
.kind(.blank),
.source(.home),
.wordCount(123)
])
}
The compiler throws an error because we need to conform the Property enum and the Kind enum to the Equatable protocol. Open Journey.swift and conform the Property enum to the Equatable protocol.
extension Journey {
struct Event {
...
}
enum Property: Equatable {
...
}
}
Open Kind.swift and conform the Kind enum to the Equatable protocol.
import Foundation
enum Kind: Equatable {
...
}
Revisit JourneyTests.swift and run the test suite. Open Journey.swift and notice that the test suite covers the properties(_:) method. Don't forget to revert the implementation of the properties(_:) method to its original implementation.
func properties(_ properties: Property...) -> Event {
Event(name: event, properties: properties)
}
What's Next?
Code coverage is helpful, but it can give you a false sense of confidence. Code coverage is nothing more than an aid when you are writing unit tests. You decide which unit tests are worth writing and it is your responsibility to spot gaps in the test suite code coverage isn't able to spot.