Overview

Extending XCTestCase – Testing Swift Optionals

No Comments

Another day, another thing to test. Whether you love or hate optionals in Swift, the reality is that you are going to have to work with them. If you are going to have to work with them you are going to have to test them (you are writing tests, right?).

If something is not easy to test, chances are, you are going to avoid testing it. Unfortunately the standard suite of XCTestCase assertions doesn’t provide support for optionals. Some blogs suggest force unwrapping the optional in the assertion:

var myOptional: String? = nil
XCTAssertEqual(myOptional!, "correct value") // Crashes

but this will crash, not fail, your tests. Depending on your build setup, this can cause various problems. Another approach is to unwrap the optional the Apple suggested way and fail if there is no value:

var myOptional: String? = nil
if let unpwarppedOptional = myOptional {
    XCTAssertEqual(unpwarppedOptional, "correct value")
}
else {
    XCTFail("Value isn't set")
}

While this works, writing this much code to test a single variable does not seem reasonable to me.

Since macro support in Swift is not what it used to be in Objective-C we can’t use the same approach we did in the previous post. Writing a function which extracts the above code would cause the test to fail in that function instead of the test. Going through the XCTestCase header file we come across this:

func recordFailureWithDescription(description: String!, inFile filePath: String!, atLine lineNumber: UInt, expected: Bool)

The documentation states:

Records a failure in the execution of the test and is used by all test assertions.

Great, so now we can fail the test outside of the test function the same way the standard XCTestCase assertions do, and still have it point to the correct place. The problem now is that the function requires the file path and line number. This is where Swifts __FILE__ and __LINE__ built-in identifiers come in.

func testSomething() {
    self.recordFailureWithDescription("Always fail", inFile: __FILE__, atLine: __LINE__, expected: true)
}

Now this in itself is not so useful, so we try and extract the assertion:

func alwaysFail() {
    self.recordFailureWithDescription("Always fail", inFile: __FILE__, atLine: __LINE__, expected: true) // failure points here
}
 
func testSomething() {
    alwaysFail()
}

The problem is that the test failure is now pointing to the contents of the alwaysFail function instead of testSomething. To remedy this we set the built-in identifiers as the default values of the file and line parameters:

func alwaysFail(file: String = __FILE__, line: UInt = __LINE__) {
    self.recordFailureWithDescription("Always fail", inFile: file, atLine: line, expected: true)
}
 
func testSomething() {
    alwaysFail() // failure points here
}

Since the file and line parameters have default values we can safely ignore them when calling the function. We now have a working test assertion extracted and can modify it to test an optional:

func NLAssertEqualOptional<T : Equatable>(theOptional: @autoclosure () -> T?, _ expression2: @autoclosure () -> T, file: String = __FILE__, line: UInt = __LINE__) {
 
    if let e = theOptional() {
        let e2 = expression2()
        if e != e2 {
            self.recordFailureWithDescription("Optional (\(e)) is not equal to (\(e2))", inFile: file, atLine: line, expected: true)
        }
    }
    else {
        self.recordFailureWithDescription("Optional value is empty", inFile: file, atLine: line, expected: true)
    }
}

This assertion will fail if the optional is nil or if the values are not equal. Note that theOptional is an @autoclosure wrapped optional which is then evaluated inside the function. Looking through the XCTestCase header shows that this is similar to the function declarations used by Apple for XCTestCase assertions.

Extract this function to an XCTestCase extension or grab the code from GitHub (which also includes some additional goodies) and plop it in to your project. Testing optionals is now a piece of cake:

var myOptional: String? = "correct value"
NLAssertEqualOptional(myOptional, "correct value") // passes
 
myOptional = nil
NLAssertEqualOptional(myOptional, "correct value") // fails inline

Caveat: Since this is an extension of XCTestCase it will require calling it with self inside closures.

Thanks to the guys at the Apple Swift blog for the inspiration, and be sure to check out the linked article if you wish to find out how to use the built-in identifiers to do some cool stuff. If you have any questions or comments, go to the comments section below or contact me on twitter @nlajic.

Comment

Your email address will not be published. Required fields are marked *