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.
At the basic level, an assertion is just a Boolean expression. It contains a true and false binary. The expression is placed into the testing program and pertains to a certain section of the software being tested. The assertion itself encompasses an expression that describes the logic of the code under test.
Assertions allow us to have Swift silently check the state of our program at runtime, but if you want to get them right you need to understand some intricacies. In this article I'll walk you through the five ways we can make assertions in Swift, and provide clear advice on which to use and when.
An assertion is a boolean expression at a specific point in a program which will be true unless there is a bug in the program. A test assertion is defined as an expression, which encapsulates some testable logic specified about a target under test.
A literal NSString , optionally with string format specifiers.
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.)
/// 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)}
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 } }
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.
Agree with nschum's comment that it doesn't seem right to unit test assert
because by default it wont be in the prod code. But if you really wanted to do it, here is the assert
version for reference:
func assert(@autoclosure condition: () -> Bool, @autoclosure _ message: () -> String = "", file: StaticString = __FILE__, line: UInt = __LINE__) { assertClosure(condition(), message(), file, line) } var assertClosure: (Bool, String, StaticString, UInt) -> () = defaultAssertClosure let defaultAssertClosure = {Swift.assert($0, $1, file: $2, line: $3)}
extension XCTestCase { func expectAssertFail(expectedMessage: String, testcase: () -> Void) { // arrange var wasCalled = false var assertionCondition: Bool? = nil var assertionMessage: String? = nil assertClosure = { condition, message, _, _ in assertionCondition = condition assertionMessage = message wasCalled = true } // act testcase() // assert XCTAssertTrue(wasCalled, "assert() was never called") XCTAssertFalse(assertionCondition!, "Expected false to be passed to the assert") XCTAssertEqual(assertionMessage, expectedMessage) // clean up assertClosure = defaultAssertClosure } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With