Skip to content

Instantly share code, notes, and snippets.

@seonar22
Forked from KingOfBrian/XCTestAndNil.md
Created December 2, 2023 13:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save seonar22/80ba6f6b3185d3edb22ed0b32496b651 to your computer and use it in GitHub Desktop.
Save seonar22/80ba6f6b3185d3edb22ed0b32496b651 to your computer and use it in GitHub Desktop.
XCTest and nil assertions

XCTest and Optional Unwrapping

XCTest is the default test harness on iOS an Apple’s other platforms. 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 than it used to be, and is my preferred test framework these days.

The Problem

One place where the XCTest assertion utilities fall a bit short has been with managing Optional variables in swift. The default use of XCTAssertNotNil don't provide any mechanism for unwrapping, 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 ugly. It's very common for ! usage to sneak in. Force-unwrapping a nil value will crash your unit tests, and the remaining unit tests will not be ran or reported to CI. The unit tests still fail, so this isn't a show stopper, but it's not ideal.

The Solution

A nice solution is possible, due to an often-overlooked feature of XCTestCase. If the test function is marked with throws, any thrown exception 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 it encounters nil, and allows the remainder of the 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, XCode points to the test function as the source of the failure, not 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 {
    guard let variable = variable else {
        XCTFail(message, file: file, line: line)
        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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment