XCTCast

The XCTest framework added the super helpful function XCTUnwrap back in Xcode 11. At the time I added a similar helper to my various projects to help smooth over the many cases where casting is required. The idea is to have a similar call site to XCTUnwrap but for use in situations where you want to verify you have the right type or fail.

This commonly happens when we are testing functions that have a return type where some of the type information is erased e.g. when a function returns a protocol or generic super class but we need to assert on a specific implementation. Another common case is if we are using mocks/doubles via subclassing, in our test we’ll often want to cast to our test type to get more information.


One such example in practice might be to check that a class implements UICollectionViewDataSource in the correct way for your application. The method that you would want to validate would be UICollectionViewDataSource.collectionView(_:cellForItemAt:) -> UICollectionViewCell. The issue we have is that the return type is UICollectionViewCell which you’ll often subclass. A safe test method might look like this:

func testFirstCellHasListing1() throws {
    let cell = controller.collectionView(collectionView, cellForItemAt: .init(item: 0, section: 0))

    guard let cell = cell as? MyCell else {
        XCTFail("Expected cell of type `MyCell` but got \(cell)")
        return
    }

    XCTAssertEqual("Item 1", cell.title)
}

In the above the middle 3 lines are just noise to handle type casting, in isolation it might not seem too bad but across a whole test suite it adds up. The above code snippet is quite wordy and it’s not uncommon to see repeated tasks performed differently overtime. Someone might come to the code base and opt for a more concise assertion that handles the casting

XCTAssertEqual("Item 1", (cell as! MyCell).title)

This is bad because if the test fails it will crash and unfortunately XCTest doesn’t handle crashes gracefully, instead of treating them as test failures it instead brings the whole test suite to a halt. It might then be wise to assume making this safer could be a case of updating to one of the following:

XCTAssertEqual("Item 1", (cell as? MyCell)?.title)
XCTAssertEqual("Item 1", try XCTUnwrap(cell as? MyCell).title)

The above lines would certainly resolve the crashing issue but the actual failures don’t really pinpoint the issue. The errors for the lines above would be something like

... -[Sample.Tests testFirstCellHasListing1] :
    XCTAssertEqual failed: ("Optional("Item 1")") is not equal to ("nil")

... -[Sample.Tests testFirstCellHasListing1] :
    XCTUnwrap failed: expected non-nil value of type "MyCell"

The first error is comparing an expected value to nil which doesn’t really hint why there is a nil without examining the code. The second error mentions the type expectation but it’s still not the main focus of the error so we’d need to inspect the code to see where the nil was introduced.


Adding our own helper

If we follow the rough design of XCTUnwrap we could end up with something like this:

extension XCTestCase {
    /// Asserts that an input can be cast to the target type and returns the cast result.
    /// - Parameters:
    ///   - input: The value to attempt to cast.
    ///   - targetType: The desired type to cast to.
    ///   - message: An optional description of the failure.
    ///   - file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called.
    ///   - line: The line number on which failure occurred. Defaults to the line number on which this function was called.
    /// - Returns: A value of type `Target`, the result of casting the given `input`.
    ///   Throws: An error when the `input` cannot be cast to the type of `Target`.
    public func XCTCast<Input, Target>(
        _ input: Input,
        as targetType: Target.Type = Target.self,
        _ message: @autoclosure () -> String = "",
        file: StaticString = #file,
        line: UInt = #line
    ) throws -> Target {
        guard let result = input as? Target else {
            let description = "XCTCast failed: expected value of type \"\(targetType)\" but got \"\(type(of: input))\"" + message()
            record(
                .init(
                    type: .assertionFailure,
                    compactDescription: description,
                    sourceCodeContext: .init(
                        location: .init(
                            filePath: file,
                            lineNumber: line
                        )
                    )
                )
            )
            throw CastingError(description: description)
        }

        return result
    }
}

struct CastingError: Error {
    let description: String
}

With the above (fairly wordy) helper tucked away in our project our call site becomes:

func testFirstCellHasListing1() throws {
    let cell = controller.collectionView(collectionView, cellForItemAt: .init(item: 0, section: 0))
    XCTAssertEqual("Item 1", try XCTCast(cell, as: MyCell.self).title)
}

This ends up being fairly concise and similar to our version above that was “safe” but gave a less helpful error

- XCTAssertEqual("Item 1", try XCTUnwrap(cell as? MyCell).title)
+ XCTAssertEqual("Item 1", try XCTCast(cell, as: MyCell.self).title)

The benefits to the new helper are that it better shows our intent when reading the test and the error message gets straight to the heart of the issue

... -[Sample.Tests testFirstCellHasListing1] :
    XCTCast failed: expected value of type "MyCell" but got "UICollectionViewCell"

Conclusion

When XCTUnwrap landed it was a real reminder that I’d once again been dealing with daily paper cuts rather than looking at making my tooling better. The XCTCast in this post was a direct reaction to that and recognising other pain points that could be easily smoothed over. Even if XCTCast isn’t useful for someone else hopefully this is a good reminder to step back and look at your current pain points and take the time to improve things for future you.

Wrapping functions in structs

We can use functions in Swift to bundle up behaviour and pass it around our application really easily. With functions we can sometimes hit specific usability examples, which can be resolved by wrapping our function in a struct. This post is 100% not a recommendation to wrap every function in a struct, but instead an examination of some cases where it might make sense.


Here’s an example of me wanting to hide a fairly complex interaction and make it more testable. In this case I want to check if the device has authorised push notifications, to keep things simple I’m not bothered about the specifics of what settings are enabled but rather just whether the system is authorised or not. I can boil this down to just injecting in a function of () async -> Bool:

World.areNotificationsAuthorized = {
    if case .authorized = await UNUserNotificationCenter.current().notificationSettings().authorizationStatus {
        return true
    } else {
        return false
    }
}

...

func performAuthorizationFlow(isAuthorized: () async -> Bool = World.areNotificationsAuthorized) {
    if (await isAuthorized()) {
        ...
    } else {
        ...
    }
}

With this approach I can test this function by simply overriding the isAuthorized function e.g.

performAuthorizationFlow(isAuthorized: { true })

// or

performAuthorizationFlow(isAuthorized: { false })

Arguably I could try and start doing some deeply nested mocking of types that I don’t own to verify that the code calls current() followed by notificationSettings() and then change the returned UNAuthorizationStatus but that involves a whole other conversation about trade offs and what I care to test.


Problem 1

This example highlights the first problem with this approach. The function performAuthorizationFlow takes in a function of () async -> Bool, which is pretty general. There could be many functions in my system that have this shape, which means I could accidentally pass the wrong function. This problem is not unique to function types and creating whole new types rather than type aliases is a common solution.

To get around this we can define a simple struct that holds onto the function like this:

struct NotificationAuthorizationStatus {
    let run: () async -> Bool
}

With this in place we can update our code to

- World.areNotificationsAuthorized = {
+ World.areNotificationsAuthorized = NotificationAuthorizationStatus {
      if case .authorized = await UNUserNotificationCenter.current().notificationSettings().authorizationStatus {
          return true
      } else {
          return false
      }
  }
  
  ...
  
- func performAuthorizationFlow(isAuthorized: () async -> Bool = World.areNotificationsAuthorized) {
+ func performAuthorizationFlow(isAuthorized: NotificationAuthorizationStatus = World.areNotificationsAuthorized) {
-     if (await isAuthorized()) {
+     if (await isAuthorized.run()) {
          ...
      } else {
          ...
      }
  }

Now we have to pass a concrete type which removes the chance of accidentally passing the wrong function to our method.


Problem 2

With a bare function we don’t really have a nice namespace to work within. In this example we have the default implementation of this authorization function that calls out to Apple’s framework. At the moment this code is floating in the breeze being assigned to the World object. We can take inspiration from Point-Free’s work on protocol witnesses and add some convenience functions on our new namespace

struct NotificationAuthorizationStatus {
    let run: () async -> Bool

    static let live = NotificationAuthorizationStatus {
        if case .authorized = await UNUserNotificationCenter.current().notificationSettings().authorizationStatus {
            return true
        } else {
            return false
        }
    }
}

With this change our original set up changes like this:

- World.areNotificationsAuthorized = NotificationAuthorizationStatus {
-     if case .authorized = await UNUserNotificationCenter.current().notificationSettings().authorizationStatus {
-         return true
-     } else {
-         return false
-     }
- }
+ World.areNotificationsAuthorized = .live

In our test target we could even add some convenience functions like this

extension NotificationAuthorizationStatus {
    static let alwaysTrue = NotificationAuthorizationStatus { true }
    static let alwaysFalse = NotificationAuthorizationStatus { false }
}

Problem 3

For the 3rd problem I’ll need a slightly different example (yes I could have used one example but repetition often helps cement ideas).

This is an issue of usability for the code calling the closure. Imagine we have the following block:

let completion: (String, String, String) -> Void

At the call site it would be pretty unclear what each of these arguments should be e.g. completion(?, ?, ?) and we’d have to trace back through our code to find out. We can improve this situation slightly by adding some documentation to the defintion so that we only need to navigate back through the code so far:

let completion: (_ email: String, _ forename: String, _ surname: String) -> Void

If we convert this to be wrapped by a struct we can see some options to improve this

struct UserCompletion {
    private let completion: (_ email: String, _ forename: String, _ surname: String) -> Void

    init(completion: @escaping (_ email: String, _ forename: String, _ surname: String) -> Void) {
        self.completion = completion
    }

    func invoke(email: String, forename: String, surname: String) {
        completion(email, forename, surname)
    }
}

With the above our call site changes from the confusing invocation with no argument labels to a normal function call with argument labels (I didn’t even think of this benefit until my friend Ellen pointed it out)

- completion("[email protected]", "Paul", "Samuels")
+ completion.invoke(email: "[email protected]", forename: "Paul", surname: "Samuels")

It’s not strongly typed and we can still pass the wrong values in each argument position but at least the labels give some guidance.

There’s another enhancement that we can take advantage of here as the extra .invoke is a bit annoying. Instead if we change the name of our method invoke to callAsFunction then this makes our struct directly invokable:

- completion.invoke(email: "[email protected]", forename: "Paul", surname: "Samuels")
+ completion(email: "[email protected]", forename: "Paul", surname: "Samuels")

Conclusion

Swift is really expressive and has a lot of features to help organise code and make things stricter. In this post we was able to make it more difficult to pass the wrong function around, gave ourselves a namespace to place code in and made our call sites a little safer.

I won’t be going off on a code rewriting spree but it’s definitely useful to have another option in my bag of tricks.

Providing an explicit type vs type inference - why not both?

Type inference is a really nice feature to have but sometimes we have to help the compiler out when what we want to write creates ambiguity.

This post uses a toy helper function that fetches remote JSON to show how we can design its api so that explicitly providing the type isn’t required when the compiler can infer types from context.


Let’s start by defining a pair of functions with a bit of duplication

func loadPost(id: Int) async throws -> Post {
    let url = URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(Post.self, from: data)
}

func loadPhoto(id: Int) async throws -> Photo {
    let url = URL(string: "https://jsonplaceholder.typicode.com/photos/\(id)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(Photo.self, from: data)
}

In the functions above the main changes are the URL to fetch and the type to attempt to JSON decode to.

We could create a helper function to remove the duplication that would look like this:

private func load<Output: Decodable>(url: URL) async throws -> Output {
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(Output.self, from: data)
}

This function is generic over an Output that must be decodable and takes care of the networking and decoding tasks. With this in place our original functions can now become one liners that call through to this helper:

func loadPost(id: Int) async throws -> Post {
    try await load(url: URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!)
}

func loadPhoto(id: Int) async throws -> Photo {
    try await load(url: URL(string: "https://jsonplaceholder.typicode.com/photos/\(id)")!)
}

With the above the compiler is happy to infer the type of Output in both cases because it can see that it needs to match the return type of the loadPost or loadPhoto functions.


This is all nice but quickly shows its inflexibility and breaks down if we change our usage slightly. If I decide that loadPost should really be changed to loadPostTitle instead as callers don’t need the full post object I would try to update my function like this

- func loadPost(id: Int) async throws -> Post {
+ func loadPostTitle(id: Int) async throws -> String {
-     try await load(url: URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!)
+     try await load(url: URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!).title
  }

With this change the compiler is no long happy and emits this error:

Generic parameter ‘Output’ could not be inferred

We can look at how JSONDecode.decode is defined to see how its api is designed. Clicking through the header we see

open func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable

We could replicate this by providing the type explicitly, the updated helper function becomes:

- private func load<Output: Decodable>(url: URL) async throws -> Output {
+ private func load<Output: Decodable>(url: URL, as type: Output.Type) async throws -> Output {
      let (data, _) = try await URLSession.shared.data(from: url)
      return try JSONDecoder().decode(Output.self, from: data)
  }

With this change the compiler now prompts us to update the call sites to explicitly provide the type to decode to

  func loadPostTitle(id: Int) async throws -> String {
-     try await load(url: URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!).title
+     try await load(url: URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!, as: Post.self).title
  }
  
  func loadPhoto(id: Int) async throws -> Photo {
-     try await load(url: URL(string: "https://jsonplaceholder.typicode.com/photos/\(id)")!)
+     try await load(url: URL(string: "https://jsonplaceholder.typicode.com/photos/\(id)")!, as: Photo.self)
  }

With the latest change we have more flexibility but if feels like we’ve lost some brevity in cases where the compiler can infer things. To bring this type inference back we can use a default argument (I think this was first shown to me by my friend Ollie Atkinson many years ago):

- private func load<Output: Decodable>(url: URL, as type: Output.Type) async throws -> Output {
+ private func load<Output: Decodable>(url: URL, as type: Output.Type = Output.self) async throws -> Output {
      let (data, _) = try await URLSession.shared.data(from: url)
      return try JSONDecoder().decode(Output.self, from: data)
  }

With this final change we get a good balance between full flexibility when we need it and type inference when the compiler can figure things out.

  func loadPostTitle(id: Int) async throws -> String {
      try await load(url: URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!, as: Post.self).title
  }
  
  func loadPhoto(id: Int) async throws -> Photo {
-     try await load(url: URL(string: "https://jsonplaceholder.typicode.com/photos/\(id)")!, as: Photo.self)
+     try await load(url: URL(string: "https://jsonplaceholder.typicode.com/photos/\(id)")!)
  }

Conclusion

Making apis intuitive and nice to use can help keep you on track solving problems whilst the code gets out of the way. We’ve probably all used apis that require all our mental energy to remember how to use them, which means we can’t focus on the problem we are trying to solve.


Sample code

Here’s a code listing with scaffolding that you can slap into a playground to explore yourself

import Foundation

struct Post: Decodable {
    let id: Int
    let title: String
}

struct Photo: Decodable {
    let albumId: Int
    let id: Int
    let title: String
}

enum Original {
    static func loadPost(id: Int) async throws -> Post {
        let url = URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(Post.self, from: data)
    }

    static func loadPhoto(id: Int) async throws -> Photo {
        let url = URL(string: "https://jsonplaceholder.typicode.com/photos/\(id)")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(Photo.self, from: data)
    }
}

print(try await Original.loadPost(id: 1))
print(try await Original.loadPhoto(id: 1))

enum HelperFunction1 {
    static func loadPost(id: Int) async throws -> Post {
        try await load(url: URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!)
    }

    static func loadPhoto(id: Int) async throws -> Photo {
        try await load(url: URL(string: "https://jsonplaceholder.typicode.com/photos/\(id)")!)
    }
    
    private static func load<Output: Decodable>(url: URL) async throws -> Output {
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(Output.self, from: data)
    }
}

print(try await HelperFunction1.loadPost(id: 1))
print(try await HelperFunction1.loadPhoto(id: 1))

enum HelperFunctionWithExplicitType {
    static func loadPostTitle(id: Int) async throws -> String {
        try await load(url: URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!, as: Post.self).title
    }

    static func loadPhoto(id: Int) async throws -> Photo {
        try await load(url: URL(string: "https://jsonplaceholder.typicode.com/photos/\(id)")!, as: Photo.self)
    }
    
    private static func load<Output: Decodable>(url: URL, as type: Output.Type) async throws -> Output {
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(Output.self, from: data)
    }
}

print(try await HelperFunctionWithExplicitType.loadPostTitle(id: 1))
print(try await HelperFunctionWithExplicitType.loadPhoto(id: 1))

enum HelperFunctionWithOptionalInference {
    static func loadPostTitle(id: Int) async throws -> String {
        try await load(url: URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!, as: Post.self).title
    }

    static func loadPhoto(id: Int) async throws -> Photo {
        try await load(url: URL(string: "https://jsonplaceholder.typicode.com/photos/\(id)")!)
    }
    
    private static func load<Output: Decodable>(url: URL, as type: Output.Type = Output.self) async throws -> Output {
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(Output.self, from: data)
    }
}

print(try await HelperFunctionWithOptionalInference.loadPostTitle(id: 1))
print(try await HelperFunctionWithOptionalInference.loadPhoto(id: 1))