XCTest is the default test harness on iOS. It provides support for organizing test cases and asserting expectations in your application code, and reporting the status of those expectations. It's not as fancy as some of the BDD frameworks like Quick
and Cedar
, but it has gotten much better, and is my preferred test framework these days.
One place where the XCTest assertion utilites fall a bit short has been with managing Optional variables in swift. The default use of XCTAssert
don't provide any mechanism for un-wrapping, easily leading to assertion checks like this:
class TestCaseDefault: XCTestCase {
func testAnOptional() {
let string: String? = nil
XCTAssertNotNil(string)
XCTAssert((string?.lengthOfBytes(using: .utf8))! > 0)
}
}
The asserts following XCTAssertNotNil
are where things can get real ugly. It's very common for !
usage to sneak in, and cause your tests to crash after a failure, which causes the rest of your tests to not run.
A nice solution is possible, due to an often overlooked feature of XCTestCase. If the test function is marked with throws
any thrown exceptions will cause the test to fail. We can use this to fail our tests using normal swift flow control mechanisms.
class TestCaseThrows: XCTestCase {
struct UnexpectedNilVariableError: Error {}
func testAnOptional() throws {
let string: String? = nil
guard let newString = string else { throw UnexpectedNilVariableError() }
// newString is Unwrapped, and things are happy
XCTAssert(newString.lengthOfBytes(using: .utf8) > 0)
}
}
This helps clean up our nil check by using more typical swift Optional expressions. This will stop the test if nil
is encountered and allow the remainder of our test to use an unwrapped value. This is a good start, except XCTestCase
doesn't report the error location correctly. If the test function throws, it does not point to the line that threw the exception. But not to worry, this is easy to clean up with a little wrapper that uses the #file
and #line
default values.
struct UnexpectedNilVariableError: Error {}
func UnwrapAndAssertNotNil<T>(_ variable: T?, message: String = "Unexpected nil variable", file: StaticString = #file, line: UInt = #line) throws -> T {
XCTAssertNotNil(variable, message, file: file, line: line)
guard let variable = variable else {
throw UnexpectedNilVariableError()
}
return variable
}
class TestCaseUnwrap: XCTestCase {
func testUnwrap() throws {
let string: String? = nil
let newString = try UnwrapAndAssertNotNil(string)
XCTAssert(newString.lengthOfBytes(using: .utf8) > 0)
}
}
Now we have a nice swifty test helper to manage nil checks. I hope this helps clean up your test code. And remember, !
is rarely a good answer, even in tests!