A blog exploring functional programming and Swift.

Introducing XCTUnimplemented

Wednesday Jun 29, 2022

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.

Dynamic XCTFail

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.

XCTUnimplemented

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())
  )
}

Start using it today!

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!


Subscribe to Point-Free

👋 Hey there! If you got this far, then you must have enjoyed this post. You may want to also check out Point-Free, a video series on functional programming and Swift.