Missing Xcode run test buttons

It happens more often than I’d like that Xcode loses the run test button in the text gutter. Often I’ll resort to closing Xcode and doing various rituals to try and get the little devils to appear again, which is really annoying if you want to avoid context switching.

When you just want to crack on I’ve had success with this flow:

  • Copy the test class name
  • Show the test navigator panel (⌘6)
  • Paste the class name into the filter (⌥⌘J followed by ⌘V)
  • Press the play button in this panel

Screen recording showing the above steps described above

Swift Parameter Packs

Parameter packs are new in Swift 5.9 and they are pretty cool. Here’s a few example uses from an initial exploration.


valuesAt

I often want to pluck out specific values from a type but don’t necessarily want to do it over multiple lines e.g. plucking details out of a Github pull request might look like this

let number = pullRequest.number
let user = pullRequest.user.login
let head = pullRequest.head.sha

The above could go on further with many more parameters but the main take away is it creates some repetitive noise and sometimes I just want a one liner. With parameter packs we can write a valuesAt function to achieve this result

let (number, user, head) = valuesAt(pullRequest, keyPaths: \.number, \.user.login, \.head.sha)

The end result is the same in that I have 3 strongly typed let bindings but I can get it onto one line.

The implementation of valuesAt looks like this:

func valuesAt<T, each U>(
    _ subject: T, 
    keyPaths keyPath: repeat KeyPath<T, each U>
) -> (repeat each U) {
    (repeat (subject[keyPath: each keyPath]))
}

decorateAround

With higher order functions in Swift it’s easy to write a function that decorates another. The issue is handling varying arity and differing return types means we previously had to write loads of function overloads. With parameter packs we can write a generic function that allows us to write wrappers inline.

Imagine I need to log the arguments and return value for a function but I don’t have access to the source so I can’t just modify the function directly. What I can do is decorate the function and then use the newly generated function in the original functions place.

let decoratedAddition: (Int, Int) -> Int = decorateAround(+) { add, a, b in
    let result = add(a, b)
    print("\(a) + \(b) = \(result)")
    return result
}

print(decoratedAddition(1, 2))

//=> 1 + 2 = 3
//=> 3

With the above the core underlying function is unchanged but I’ve added additional observability. With this particular set up the decorateAround actually gives the caller lots of flexibility as they can also optionally inspect/modify the arguments to the wrapped function and then modify the result.

The code to achieve this triggers my semantic satiation for the words repeat and each but here it is in all its glory

func decorateAround<each Argument, Return>(
    _ function: @escaping (repeat each Argument) -> Return,
    around: @escaping ((repeat each Argument) -> Return, repeat each Argument) -> Return
) -> (repeat each Argument) -> Return {
    { (argument: repeat each Argument) in
        around(function, repeat each argument)
    }
}

We could go further and create helpers that make it simple to decoratePre and decoratePost and only use the decorateAround variant when we need full flexibility.


memoize

With the general pattern of decoration there are other things we can expand on. One such function would be to memoize expensive computations so if we call a decorated function with the same inputs multiple times we expect the computation to be performed only once. One example might be loading a resource from disk and keeping it in a local cache to avoid the disk IO when the same file is requested

let memoizedLoadImage = memoize(loadImage)

memoizedLoadImage(URL(filePath: "some-url"))
memoizedLoadImage(URL(filePath: "some-url"))

memoizedLoadImage(URL(filePath: "other-url"))

In the above example the image at some-url will only have the work performed to load it the first time, on the subsequent call the in memory cached result will be returned. The final call to other-url will not have any result in the cache and so would trigger a disk load.

In order to build this one we have to get a little more inventive with things as the cache is a Dictionary so we need to build a key somehow but tuples are not Hashable. I ended up building an array for the key that has all the arguments type erased to AnyHashable. The code looks like this:

func memoize<each Argument: Hashable, Return>(
    _ function: @escaping (repeat each Argument) -> Return
) -> (repeat each Argument) -> Return {
    var storage = [AnyHashable: Return]()
    
    return { (argument: repeat each Argument) in
        var key = [AnyHashable]()
        repeat key.append(AnyHashable(each argument))
        
        if let result = storage[key] {
            return result
        } else {
            let result = function(repeat each argument)
            storage[key] = result
            return result
        }
    }
}

Conclusion

Parameter packs are an interesting feature - I’m not sure the above code snippets are particularly good or even sensible to use but I hope it helps people get their toe in the door on using the feature and potentially coming up with stronger use cases than I’ve imagined.

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.