Testing assertion in Swift

hpique picture hpique · Aug 27, 2014 · Viewed 11.4k times · Source

I'm writing unit tests for a method that has an assertion. The Swift Language guide recommends using assertions for "invalid conditions":

Assertions cause your app to terminate and are not a substitute for designing your code in such a way that invalid conditions are unlikely to arise. Nonetheless, in situations where invalid conditions are possible, an assertion is an effective way to ensure that such conditions are highlighted and noticed during development, before your app is published.

I want to test the failure case.

However, there is not XCTAssertThrows in Swift (as of Beta 6). How can I write an unit test that tests that an assertion fails?

Edit

As per @RobNapier's suggestion, I tried wrapping XCTAssertThrows in an Objective-C method and calling this method from Swift. This doesn't work as the macro does not catch the fatal error caused by assert, and thus the test crashes.

Answer

nschum picture nschum · Jul 10, 2015

assert and its sibling precondition don't throw exceptions cannot be "caught" (even with Swift 2's error handling).

A trick you can use is to write your own drop-in replacement that does the same thing but can be replaced for tests. (If you're worried about performance, just #ifdef it away for release builds.)

custom precondition

/// Our custom drop-in replacement `precondition`.
///
/// This will call Swift's `precondition` by default (and terminate the program).
/// But it can be changed at runtime to be tested instead of terminating.
func precondition(@autoclosure condition: () -> Bool, @autoclosure _ message: () -> String = "", file: StaticString = __FILE__, line: UWord = __LINE__) {
    preconditionClosure(condition(), message(), file, line)
}

/// The actual function called by our custom `precondition`.
var preconditionClosure: (Bool, String, StaticString, UWord) -> () = defaultPreconditionClosure
let defaultPreconditionClosure = {Swift.precondition($0, $1, file: $2, line: $3)}

test helper

import XCTest

extension XCTestCase {
    func expectingPreconditionFailure(expectedMessage: String, @noescape block: () -> ()) {

        let expectation = expectationWithDescription("failing precondition")

        // Overwrite `precondition` with something that doesn't terminate but verifies it happened.
        preconditionClosure = {
            (condition, message, file, line) in
            if !condition {
                expectation.fulfill()
                XCTAssertEqual(message, expectedMessage, "precondition message didn't match", file: file.stringValue, line: line)
            }
        }

        // Call code.
        block();

        // Verify precondition "failed".
        waitForExpectationsWithTimeout(0.0, handler: nil)

        // Reset precondition.
        preconditionClosure = defaultPreconditionClosure
    }
}

example

func doSomething() {
    precondition(false, "just not true")
}

class TestCase: XCTestCase {
    func testExpectPreconditionFailure() {
        expectingPreconditionFailure("just not true") {
            doSomething();
        }
    }
}

(gist)

Similar code will work for assert, of course. However, since you're testing the behavior, you obviously want it to be part of your interface contract. You don't want optimized code to violate it, and assert will be optimized away. So better use precondition here.