A black and white image of the author Kolja Dummann

a Functional Approach to Asserts in Swift.

While I was coding some tests for my side project I thought about if I can find a way to write my tests in a way that fits better to my way of thinking while I code.

When I write my tests my way of thinking is:

What do I want to test and what’s the expected result?

And I like it when my code and the way in which I write it reflects that. But if we have a look a usual XCTestCase we see that it’s not quite like that:

let mng = 
let a = mng.createRowAtTop()
let b = 
XCTAssertNotNil(a)
XCTAssertNil(a?.above)
XCTAssertEquals(a?.below, b)
XCTAssertEquals(b?.above, a)

For the first assert it might still be ok because I simply assume that the result is not nil but for the later I first have to think about what I want to test, then derive the expected result, write the result and then the thing I want to test. To me this was often somewhat distracting. It does not fit the flow that’s going in my head.

Since there isn’t much meta programming going on in swift the only way to something would be custom operators. In my head there was an idea about something like this:


a <!> nil
a?.above <=> nil
a?.below <=> b
b?.above <=> a

What I did I played around a bit with some custom operators. I created (concatenated random chars that are valid for operators) multiple operators to write my assertions. I came up with <=> for assert equals and <!> for assert not equals. The implementation is straight forward:

infix operator <=> { associativity left }
func <=><T : Equatable> (left : T?, right : T?) {
   XCTAssertEqual(left, right)
}

infix operator <!> { associativity left }
func <!><T : Equatable> (left : T?, right : T?) {
   XCTAssertNotEqual(left, right)
}

This basically allows to write the code seen above. But it has some downsides. If one of these assertions fails the location where it failed will be in the definition of the custom operator and not at the place where the operator was used. What I need is a way to persevere the location information. XCTAssertEqual and its friends allow to hand the location to them as parameters but infix operator can only have two arguments, even if all the others have default values. Because of that #file and #line aren’t an option on the operator.

What I need is a way to capture the information with a function call and then hand it over to the operator which then passes them to XCTAssertEqual. First thing I did was I changed the operator definition:

func <=><T : Equatable> (left : T?, right : (value :T?, file: StaticString, msg:  String, line : UInt)) {
   XCTAssertEqual(left, right.value, right.msg, file: right.file, line: right.line)
}

The operator now takes a value on the left and a tuple of the expected value and the location information on the right. Next thing I need is a function to call so that I can capture the context:

func equals<T>(value : T?, message : String = "", file : StaticString = #file, line : UInt = #line) -> (T?, StaticString, String, UInt) {
    return (value, file, message, line);
}

I can write something like:

a?.below <=> equals(b)
b?.above <=> equals(a)

Now that I call the equals method on the right I can capture the information needed for the assert at the right place. But there is a bit of a problem, I have to call the equals method on the right side of the assert. This seems odd because my initial thought was that I will have different operators for the different kinds of asserts. I decided to go with a single operator for asserts and have different methods for creating the expectation on the right. To so I simply introduced a enum to encode the assert operation:

enum AssertOp {
    case Equals
    case NotEquals
}

infix operator <=> { associativity left }
/**
 Asserts that the left hand side equals the right hand side and returns the left hand side.
 */
func <=><T : Equatable> (left : T?, right : (value :T?, op: AssertOp, file: StaticString, msg:  String, line : UInt)) -> T? {
    switch  right.op {
        case .Equals:
            XCTAssertEqual(left, right.value, right.msg, file: right.file, line: right.line)
        case .NotEquals:
            XCTAssertNotEqual(left, right.value, right.msg, file: right.file, line: right.line)
    }
    return left
}

func equals<T>(value : T?, message : String = "", file : StaticString = #file, line : UInt = #line) -> (T?, AssertOp, StaticString, String, UInt) {
    return (value, .Equals, file, message, line)
}

func notEquals<T>(value : T?, message : String = "", file : StaticString = #file, line : UInt = #line) -> (T?, AssertOp, StaticString, String, UInt) {
    return (value, .NotEquals, file, message, line)
}

So far, so good. I can now write assertion about equality but this approach won’t work for cases where I want to assert that something is nil or not. In theses cases there is no value on the right side that I could use to infer T from. But I can simply overload the operator with another one. This overload will take a different enum and no value.

enum SimpleAssertOp {
    case NotNil
    case Nil
}
func <=><T : Equatable> (left : T?, right : (op: SimpleAssertOp, file: StaticString, msg:  String, line : UInt)) -> T? {  }

And then two function to capture the context and create the real tuple:

func notNil(message : String = "", file : StaticString = #file, line : UInt = #line) -> (SimpleAssertOp, StaticString, String, UInt){
    return (.NotNil, file, message, line);
}

func isNil(message : String = "", file : StaticString = #file, line : UInt = #line) -> (SimpleAssertOp, StaticString, String, UInt) {
    return (.Nil, file, message, line);
}

The function to create a nil expectation can’t be named nil because it’s a keyword in the language. Now I can write and read my assertions from left to right just like I think when I write my tests:

a <=> isNil
a?.above <=> nil
a?.below <=> b
b?.above <=> a

For this use case the operators did the job in a good way but this is as far as it gets. The meta programming capabilities of swift are somewhat limited. You can’t create custom macros or hook into the compiler otherwise. Would be nice if one could get access to the AST and apply transformations typesafe on it like in rust.

The full example is here. Simply paste it into a playground and explore it.