We have just released 0.3.0 of our XCTest Dynamic Overlay library, which brings a new tool that aids in constructing stronger dependencies for tests.
We first open sourced XCTest Dynamic Overlay over a year ago, and its sole purpose at that time was to allow using XCTFail
in application code. This allows you to write test helpers right alongside feature code without importing XCTest, which otherwise does not compile for simulators or devices.
For example, suppose you have a lightweight dependency for tracking analytics in your client:
struct AnalyticsClient {
var track: (Event) -> Void
struct Event: Equatable {
var name: String
var properties: [String: String]
}
}
For the production app you can use a “live” version of the dependency that actually sends data to an analytics server:
extension AnalyticsClient {
static let live = Self(
track: { event in
// Send event to server
}
)
}
But for tests we can use an “unimplemented” version of the analytics client, which allows us to prove when we don’t expect a dependency to be used in a test:
import XCTestDynamicOverlay
extension AnalyticsClient {
static let unimplemented = Self(
track: { _ in XCTFail("\(Self.self).track is unimplemented.") }
)
}
If you pass along AnalyticsClient.unimplemented
to your feature in tests and the test passes, you have proof that the slice of your feature you are exercising definitely does not track any analytics. That is incredibly powerful.
Without XCTest Dynamic Overlay you would need to extract this unimplemented instance to its own module just so that it could only be imported in tests. That causes a proliferation of unnecessary modules for something that should be quite simple.
The new XCTUnimplemented
function builds on XCTest Dynamic Overlay’s core functionality by making it even easier to construct unimplemented dependencies. It is a massively overloaded function that allows you to construct a function of any form (up to 5 arguments, throwing and non-throwing, async and non-async) that immediately fails the test suite if it is ever invoked.
For example, the Analytics.unimplemented
instance can now be constructed simply as:
import XCTestDynamicOverlay
extension AnalyticsClient {
static let unimplemented = Self(
track: XCTUnimplemented("\(Self.self).track")
)
}
And this helper really shines with more complicated dependencies with lots of endpoints:
struct AppDependencies {
var date: () -> Date = Date.init,
var fetchUser: (User.ID) async throws -> User,
var uuid: () -> UUID = UUID.init
}
extension AppDependencies {
static let unimplemented = Self(
date: XCTUnimplemented("\(Self.self).date", placeholder: Date()),
fetchUser: XCTUnimplemented("\(Self.self).fetchUser"),
uuid: XCTUnimplemented("\(Self.self).uuid", placeholder: UUID())
)
}
Add XCTest Dynamic Overlay to your project today to start building testing tools right along side your application code!
If you are interested in learning more about the concept of “unimplemented” dependencies, be sure to check out our episode on the topic!