Unit Test fatalError in Swift
The idea is to replace the built-in fatalError
function with your own, which is replaced during a unit test's execution, so that you run unit test assertions in it.
However, the tricky part is that fatalError
is @noreturn
, so you need to override it with a function which never returns.
Override fatalError
In your app target only (don't add to the unit test target):
// overrides Swift global `fatalError`@noreturn func fatalError(@autoclosure message: () -> String = "", file: StaticString = __FILE__, line: UInt = __LINE__) { FatalErrorUtil.fatalErrorClosure(message(), file, line) unreachable()}/// This is a `noreturn` function that pauses forever@noreturn func unreachable() { repeat { NSRunLoop.currentRunLoop().run() } while (true)}/// Utility functions that can replace and restore the `fatalError` global function.struct FatalErrorUtil { // Called by the custom implementation of `fatalError`. static var fatalErrorClosure: (String, StaticString, UInt) -> () = defaultFatalErrorClosure // backup of the original Swift `fatalError` private static let defaultFatalErrorClosure = { Swift.fatalError($0, file: $1, line: $2) } /// Replace the `fatalError` global function with something else. static func replaceFatalError(closure: (String, StaticString, UInt) -> ()) { fatalErrorClosure = closure } /// Restore the `fatalError` global function back to the original Swift implementation static func restoreFatalError() { fatalErrorClosure = defaultFatalErrorClosure }}
Extension
Add the following extension to your unit test target:
extension XCTestCase { func expectFatalError(expectedMessage: String, testcase: () -> Void) { // arrange let expectation = expectationWithDescription("expectingFatalError") var assertionMessage: String? = nil // override fatalError. This will pause forever when fatalError is called. FatalErrorUtil.replaceFatalError { message, _, _ in assertionMessage = message expectation.fulfill() } // act, perform on separate thead because a call to fatalError pauses forever dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), testcase) waitForExpectationsWithTimeout(0.1) { _ in // assert XCTAssertEqual(assertionMessage, expectedMessage) // clean up FatalErrorUtil.restoreFatalError() } }}
Testcase
class TestCase: XCTestCase { func testExpectPreconditionFailure() { expectFatalError("boom!") { doSomethingThatCallsFatalError() } }}
I got the idea from this post about unit testing assert
and precondition
:Testing assertion in Swift
Swift 4 and Swift 3
Based on Ken's answer.
In your App Target add the following:
import Foundation// overrides Swift global `fatalError`public func fatalError(_ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) -> Never { FatalErrorUtil.fatalErrorClosure(message(), file, line) unreachable()}/// This is a `noreturn` function that pauses foreverpublic func unreachable() -> Never { repeat { RunLoop.current.run() } while (true)}/// Utility functions that can replace and restore the `fatalError` global function.public struct FatalErrorUtil { // Called by the custom implementation of `fatalError`. static var fatalErrorClosure: (String, StaticString, UInt) -> Never = defaultFatalErrorClosure // backup of the original Swift `fatalError` private static let defaultFatalErrorClosure = { Swift.fatalError($0, file: $1, line: $2) } /// Replace the `fatalError` global function with something else. public static func replaceFatalError(closure: @escaping (String, StaticString, UInt) -> Never) { fatalErrorClosure = closure } /// Restore the `fatalError` global function back to the original Swift implementation public static func restoreFatalError() { fatalErrorClosure = defaultFatalErrorClosure }}
In your test target add the following:
import Foundationimport XCTestextension XCTestCase { func expectFatalError(expectedMessage: String, testcase: @escaping () -> Void) { // arrange let expectation = self.expectation(description: "expectingFatalError") var assertionMessage: String? = nil // override fatalError. This will pause forever when fatalError is called. FatalErrorUtil.replaceFatalError { message, _, _ in assertionMessage = message expectation.fulfill() unreachable() } // act, perform on separate thead because a call to fatalError pauses forever DispatchQueue.global(qos: .userInitiated).async(execute: testcase) waitForExpectations(timeout: 0.1) { _ in // assert XCTAssertEqual(assertionMessage, expectedMessage) // clean up FatalErrorUtil.restoreFatalError() } }}
Test case:
class TestCase: XCTestCase { func testExpectPreconditionFailure() { expectFatalError(expectedMessage: "boom!") { doSomethingThatCallsFatalError() } }}
Thanks to nschum and Ken Ko for the idea behind this answer.
Here is a gist for how to do it.
This answer is not just for fatal error. It's also for the other assertion methods (assert
, assertionFailure
, precondition
, preconditionFailure
and fatalError
)
1. Drop ProgrammerAssertions.swift
to the target of your app or framework under test. Just besides your source code.
ProgrammerAssertions.swift
import Foundation/// drop-in replacementspublic func assert(@autoclosure condition: () -> Bool, @autoclosure _ message: () -> String = "", file: StaticString = __FILE__, line: UInt = __LINE__) { Assertions.assertClosure(condition(), message(), file, line)}public func assertionFailure(@autoclosure message: () -> String = "", file: StaticString = __FILE__, line: UInt = __LINE__) { Assertions.assertionFailureClosure(message(), file, line)}public func precondition(@autoclosure condition: () -> Bool, @autoclosure _ message: () -> String = "", file: StaticString = __FILE__, line: UInt = __LINE__) { Assertions.preconditionClosure(condition(), message(), file, line)}@noreturn public func preconditionFailure(@autoclosure message: () -> String = "", file: StaticString = __FILE__, line: UInt = __LINE__) { Assertions.preconditionFailureClosure(message(), file, line) runForever()}@noreturn public func fatalError(@autoclosure message: () -> String = "", file: StaticString = __FILE__, line: UInt = __LINE__) { Assertions.fatalErrorClosure(message(), file, line) runForever()}/// Stores custom assertions closures, by default it points to Swift functions. But test target can override them.public class Assertions { public static var assertClosure = swiftAssertClosure public static var assertionFailureClosure = swiftAssertionFailureClosure public static var preconditionClosure = swiftPreconditionClosure public static var preconditionFailureClosure = swiftPreconditionFailureClosure public static var fatalErrorClosure = swiftFatalErrorClosure public static let swiftAssertClosure = { Swift.assert($0, $1, file: $2, line: $3) } public static let swiftAssertionFailureClosure = { Swift.assertionFailure($0, file: $1, line: $2) } public static let swiftPreconditionClosure = { Swift.precondition($0, $1, file: $2, line: $3) } public static let swiftPreconditionFailureClosure = { Swift.preconditionFailure($0, file: $1, line: $2) } public static let swiftFatalErrorClosure = { Swift.fatalError($0, file: $1, line: $2) }}/// This is a `noreturn` function that runs forever and doesn't return./// Used by assertions with `@noreturn`.@noreturn private func runForever() { repeat { NSRunLoop.currentRunLoop().run() } while (true)}
2. Drop XCTestCase+ProgrammerAssertions.swift
to your test target. Just besides your test cases.
XCTestCase+ProgrammerAssertions.swift
import Foundationimport XCTest@testable import Assertionsprivate let noReturnFailureWaitTime = 0.1public extension XCTestCase { /** Expects an `assert` to be called with a false condition. If `assert` not called or the assert's condition is true, the test case will fail. - parameter expectedMessage: The expected message to be asserted to the one passed to the `assert`. If nil, then ignored. - parameter file: The file name that called the method. - parameter line: The line number that called the method. - parameter testCase: The test case to be executed that expected to fire the assertion method. */ public func expectAssert( expectedMessage: String? = nil, file: StaticString = __FILE__, line: UInt = __LINE__, testCase: () -> Void ) { expectAssertionReturnFunction("assert", file: file, line: line, function: { (caller) -> () in Assertions.assertClosure = { condition, message, _, _ in caller(condition, message) } }, expectedMessage: expectedMessage, testCase: testCase) { () -> () in Assertions.assertClosure = Assertions.swiftAssertClosure } } /** Expects an `assertionFailure` to be called. If `assertionFailure` not called, the test case will fail. - parameter expectedMessage: The expected message to be asserted to the one passed to the `assertionFailure`. If nil, then ignored. - parameter file: The file name that called the method. - parameter line: The line number that called the method. - parameter testCase: The test case to be executed that expected to fire the assertion method. */ public func expectAssertionFailure( expectedMessage: String? = nil, file: StaticString = __FILE__, line: UInt = __LINE__, testCase: () -> Void ) { expectAssertionReturnFunction("assertionFailure", file: file, line: line, function: { (caller) -> () in Assertions.assertionFailureClosure = { message, _, _ in caller(false, message) } }, expectedMessage: expectedMessage, testCase: testCase) { () -> () in Assertions.assertionFailureClosure = Assertions.swiftAssertionFailureClosure } } /** Expects an `precondition` to be called with a false condition. If `precondition` not called or the precondition's condition is true, the test case will fail. - parameter expectedMessage: The expected message to be asserted to the one passed to the `precondition`. If nil, then ignored. - parameter file: The file name that called the method. - parameter line: The line number that called the method. - parameter testCase: The test case to be executed that expected to fire the assertion method. */ public func expectPrecondition( expectedMessage: String? = nil, file: StaticString = __FILE__, line: UInt = __LINE__, testCase: () -> Void ) { expectAssertionReturnFunction("precondition", file: file, line: line, function: { (caller) -> () in Assertions.preconditionClosure = { condition, message, _, _ in caller(condition, message) } }, expectedMessage: expectedMessage, testCase: testCase) { () -> () in Assertions.preconditionClosure = Assertions.swiftPreconditionClosure } } /** Expects an `preconditionFailure` to be called. If `preconditionFailure` not called, the test case will fail. - parameter expectedMessage: The expected message to be asserted to the one passed to the `preconditionFailure`. If nil, then ignored. - parameter file: The file name that called the method. - parameter line: The line number that called the method. - parameter testCase: The test case to be executed that expected to fire the assertion method. */ public func expectPreconditionFailure( expectedMessage: String? = nil, file: StaticString = __FILE__, line: UInt = __LINE__, testCase: () -> Void ) { expectAssertionNoReturnFunction("preconditionFailure", file: file, line: line, function: { (caller) -> () in Assertions.preconditionFailureClosure = { message, _, _ in caller(message) } }, expectedMessage: expectedMessage, testCase: testCase) { () -> () in Assertions.preconditionFailureClosure = Assertions.swiftPreconditionFailureClosure } } /** Expects an `fatalError` to be called. If `fatalError` not called, the test case will fail. - parameter expectedMessage: The expected message to be asserted to the one passed to the `fatalError`. If nil, then ignored. - parameter file: The file name that called the method. - parameter line: The line number that called the method. - parameter testCase: The test case to be executed that expected to fire the assertion method. */ public func expectFatalError( expectedMessage: String? = nil, file: StaticString = __FILE__, line: UInt = __LINE__, testCase: () -> Void) { expectAssertionNoReturnFunction("fatalError", file: file, line: line, function: { (caller) -> () in Assertions.fatalErrorClosure = { message, _, _ in caller(message) } }, expectedMessage: expectedMessage, testCase: testCase) { () -> () in Assertions.fatalErrorClosure = Assertions.swiftFatalErrorClosure } } // MARK:- Private Methods private func expectAssertionReturnFunction( functionName: String, file: StaticString, line: UInt, function: (caller: (Bool, String) -> Void) -> Void, expectedMessage: String? = nil, testCase: () -> Void, cleanUp: () -> () ) { let expectation = expectationWithDescription(functionName + "-Expectation") var assertion: (condition: Bool, message: String)? = nil function { (condition, message) -> Void in assertion = (condition, message) expectation.fulfill() } // perform on the same thread since it will return testCase() waitForExpectationsWithTimeout(0) { _ in defer { // clean up cleanUp() } guard let assertion = assertion else { XCTFail(functionName + " is expected to be called.", file: file.stringValue, line: line) return } XCTAssertFalse(assertion.condition, functionName + " condition expected to be false", file: file.stringValue, line: line) if let expectedMessage = expectedMessage { // assert only if not nil XCTAssertEqual(assertion.message, expectedMessage, functionName + " called with incorrect message.", file: file.stringValue, line: line) } } } private func expectAssertionNoReturnFunction( functionName: String, file: StaticString, line: UInt, function: (caller: (String) -> Void) -> Void, expectedMessage: String? = nil, testCase: () -> Void, cleanUp: () -> () ) { let expectation = expectationWithDescription(functionName + "-Expectation") var assertionMessage: String? = nil function { (message) -> Void in assertionMessage = message expectation.fulfill() } // act, perform on separate thead because a call to function runs forever dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), testCase) waitForExpectationsWithTimeout(noReturnFailureWaitTime) { _ in defer { // clean up cleanUp() } guard let assertionMessage = assertionMessage else { XCTFail(functionName + " is expected to be called.", file: file.stringValue, line: line) return } if let expectedMessage = expectedMessage { // assert only if not nil XCTAssertEqual(assertionMessage, expectedMessage, functionName + " called with incorrect message.", file: file.stringValue, line: line) } } }}
3. Use assert
, assertionFailure
, precondition
, preconditionFailure
and fatalError
normally as you always do.
For example: If you have a function that does a division like the following:
func divideFatalError(x: Float, by y: Float) -> Float { guard y != 0 else { fatalError("Zero division") } return x / y}
4. Unit test them with the new methods expectAssert
, expectAssertionFailure
, expectPrecondition
, expectPreconditionFailure
and expectFatalError
.
You can test the 0 division with the following code.
func testFatalCorrectMessage() { expectFatalError("Zero division") { divideFatalError(1, by: 0) }}
Or if you don't want to test the message, you simply do.
func testFatalErrorNoMessage() { expectFatalError() { divideFatalError(1, by: 0) }}