A microlibrary that imitates an XCTFail and it can be called from anywhere
XCTest Interface Adapter
XCTest Interface Adapter is a microlibrary that imitates an XCTFail
and it can be called from anywhere.
import XCTestInterfaceAdapter
Example
Imagine that you have an analytics dependency that is used all over your application:
// MARK: - AnalyticsClient
protocol AnalyticsClient {
/// Log some analytics event
///
/// - Parameter event: some event
func log(_ event: AnalyticsEvent)
}
...
// MARK: - AnalyticsClientImplementation
final class AnalyticsClientImplementation {
// MARK: - Properties
/// All available analytics engines (Firebase, CloudKit etc.)
private let engines: [AnalyticsEngine]
// MARK: - Initializers
/// Default initializer
///
/// - Parameter engines: all available analytics engines (Firebase, CloudKit etc.)
init(engines: [AnalyticsEngine]) {
self.engines = engines
}
}
// MARK: - AnalyticsClient
extension AnalyticsClientImplementation: AnalyticsClient {
func log(_ event: AnalyticsEvent) {
engines.forEach {
$0.sendAnalyticsEvent(named: event.name, metadata: event.metadata)
}
}
}
If you are disciplined about injecting dependencies, you probably have a lot of objects that take an analytics client as an argument (or maybe some other fancy form of DI):
final class LoginViewController: UIViewController {
...
init(analytics: AnalyticsClient) {
...
}
...
}
When testing this view model you will need to provide an analytics client. Typically this means you will construct some kind of “test” analytics client that buffers events into an array, rather than sending live events to a server, so that you can assert on what events were tracked during a test:
func testLogin() {
let viewModel = LoginViewModel(
analytics: AnalyticsClientImplementation(engines: engines)
)
...
XCTAssertEqual(loggedEvents, [.loginSuccess])
}
This works really well, and it’s a great way to get test coverage on something that is notoriously difficult to test.
However, some tests may not use analytics at all. It would make the test suite stronger if the tests that don’t use the client could prove that it’s never used. This would mean when new events are tracked you could be instantly notified of which test cases need to be updated.
One way to do this is to create an instance of the AnalyticsClient
type that simply performs an XCTFail
inside the log
endpoint:
import XCTest
// MARK: - FailingAnalyticsManager
final class FailingAnalyticsManager {
}
// MARK: - AnalyticsManager
extension FailingAnalyticsManager: AnalyticsManager {
func log(_ event: AnalyticsEvent) {
XCTFail("AnalyticsClient.log is unimplemented.")
}
}
With this you can write a test that proves analytics are never tracked, and even better you don’t have to worry about buffering events into an array anymore:
func testValidation() {
let viewModel = LoginViewModel(
analytics: FailingAnalyticsManager()
)
...
}
You cannot ship this code with the target that defines AnalyticsClient
. You either need to extract it out to a test support module (which means AnalyticsClient
must also be extracted), or the code must be confined to a test target and thus not shareable.
However, with XCTestInterfaceAdapter
we can do it. We can define both the client type and the failing instance right next to each in application code without needing to extract out needless modules or targets:
// MARK: - AnalyticsClient
protocol AnalyticsClient {
/// Log some analytics event
///
/// - Parameter event: some event
func log(_ event: AnalyticsEvent)
}
import XCTestInterfaceAdapter
// MARK: - FailingAnalyticsManager
final class FailingAnalyticsManager {
}
// MARK: - AnalyticsManager
extension FailingAnalyticsManager: AnalyticsManager {
func log(_ event: AnalyticsEvent) {
XCTFail("AnalyticsClient.log is unimplemented.")
}
}
License
This library is released under the MIT license. See LICENSE for details.