//

Extending XCTestCase – Testing Swift Optionals

2.10.2014 | 3 minutes of reading time

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:

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

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:

1var myOptional: String? = nil
2if let unpwarppedOptional = myOptional {
3    XCTAssertEqual(unpwarppedOptional, "correct value")
4}
5else {
6    XCTFail("Value isn't set")
7}
8

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:

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

The documentation states:

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

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.

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

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

1func alwaysFail() {
2    self.recordFailureWithDescription("Always fail", inFile: __FILE__, atLine: __LINE__, expected: true) // failure points here
3}
4 
5func testSomething() {
6    alwaysFail()
7}
8

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:

1func alwaysFail(file: String = __FILE__, line: UInt = __LINE__) {
2    self.recordFailureWithDescription("Always fail", inFile: file, atLine: line, expected: true)
3}
4 
5func testSomething() {
6    alwaysFail() // failure points here
7}
8

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:

1func NLAssertEqualOptional<T : Equatable>(theOptional: @autoclosure () -> T?, _ expression2: @autoclosure () -> T, file: String = __FILE__, line: UInt = __LINE__) {
2 
3    if let e = theOptional() {
4        let e2 = expression2()
5        if e != e2 {
6            self.recordFailureWithDescription("Optional (\(e)) is not equal to (\(e2))", inFile: file, atLine: line, expected: true)
7        }
8    }
9    else {
10        self.recordFailureWithDescription("Optional value is empty", inFile: file, atLine: line, expected: true)
11    }
12}
13

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:

1var myOptional: String? = "correct value"
2NLAssertEqualOptional(myOptional, "correct value") // passes
3 
4myOptional = nil
5NLAssertEqualOptional(myOptional, "correct value") // fails inline
6

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 .

share post

Likes

0

//

More articles in this subject area\n

Discover exciting further topics and let the codecentric world inspire you.

//

Gemeinsam bessere Projekte umsetzen

Wir helfen Deinem Unternehmen

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.