44

How to implement unit test for a fatalError code path in Swift?

For example, I've the following swift code

func divide(x: Float, by y: Float) -> Float {

    guard y != 0 else {
        fatalError("Zero division")
    }

    return x / y
}

I want to unit test the case when y = 0.

Note, I want to use fatalError not any other assertion function.

Blazej SLEBODA
  • 8,936
  • 7
  • 53
  • 93
mohamede1945
  • 7,092
  • 6
  • 47
  • 61

5 Answers5

24

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

Community
  • 1
  • 1
Ken Ko
  • 1,517
  • 2
  • 15
  • 21
  • That seems very promising. I will give it a shot later today and mark it as answered. – mohamede1945 Dec 16 '15 at 09:10
  • Made an edit to fix up a couple of compile issues, and also refactored to wrap up in a util struct so that there is less global state – Ken Ko Dec 17 '15 at 10:42
  • 1
    It's not clear to me if/how to update this for Swift 3's move from `@noreturn` to `-> Never`. Maybe I'm just missing something -- how do you end the `unreachable` function's execution? – Richard Jan 29 '17 at 07:08
  • @GuyDaher The basic idea is to `waitForExpectationsWithTimeout` with an `XTCFail` in its `handler` block, and hope that your `Never` gets called within that amount of time. something like `doSomething() waitForExpectations(timeout: ASYNC_TIMEOUT, handler: {error in if let error = error { XCTFail(error.localizedDescription) } ` – Richard May 19 '17 at 16:30
  • @GuyDaher I also moved my `Never` function into a delegate protocol so that I could set my test class as the delegate for test purposes, and it would fulfill the expectation. – Richard May 19 '17 at 16:30
  • @AlexBartiş Check my answer below for Swift 3 – Guy Daher May 23 '17 at 16:35
  • FWIW, that technique masks, rather than overriding, fatalError. Overriding fatalError would mean that code from other modules (including the standard library itself) would end up invoking your function when they call fatalError, but this doesn't do that. If masking is good enough for your purposes, of course, then more power to ya! – Dave Abrahams Mar 16 '18 at 23:59
  • I have tried this method and found that it is not effective for testing Combine publishers that may trigger a fatalError, because unfortunately every other test running as a result of the RunLoop hack causes some weird problems that are very difficult to pinpoint. – scaly Jun 22 '21 at 20:42
  • this solution doesn't work if the test case is `async` – JAHelia Mar 15 '23 at 11:12
20

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 forever
public 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 Foundation
import XCTest

extension 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()
        }
    }
}
Guy Daher
  • 5,526
  • 5
  • 42
  • 67
  • Works great! Just need to update the sample with ```expectFatalError(expectedMessage: "boom!")``` – trusk May 24 '17 at 16:11
  • 1
    What's the most elegant way to get rid of the **"Will never be executed"** warning around `unreachable()`? – Nicolas Miari Jul 09 '18 at 10:59
  • The extension to `XCTestCase` uses the `FatalErrorUtil ` struct; I had to add `@testable import MyFramework` to the imports (I'm testing a framework target). – Nicolas Miari Jul 09 '18 at 11:02
  • Thanks! Any ideas for using this on the main thread? E.g., I am testing building a view from XIB and this code must also be called on the main thread. – aunnnn Jul 09 '18 at 13:04
  • 1
    This leaves a discarded thread in GCD for each call to `expectFatalError`, and these threads may spin given `RunLoop.current.run()` can return immediately. I fixed this by using a `Thread` rather than `DispatchQueue`, and exited the thread in `replaceFatalError` by calling `Thread.exit()`. – jedwidz Aug 25 '18 at 15:12
13

Thanks to nschum and Ken Ko for the idea behind this answer.

Here is a gist for how to do it.

Here is an example project.

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 replacements

public 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 Foundation
import XCTest
@testable import Assertions

private let noReturnFailureWaitTime = 0.1

public 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)
    }
}
Community
  • 1
  • 1
mohamede1945
  • 7,092
  • 6
  • 47
  • 61
  • 1
    I don't why I had to increase the `noReturnFailureWaitTime` value in order to the unit tests to continue. But it works. Thx – Vaseltior Feb 21 '16 at 13:09
  • Isn't step `1` too limiting? It forces you to have one target just for unit testing and another for actual distribution, i.e. to testers. Otherwise if testers hit `fatalError` the app will hang but not fail. Alternatively, the code with custom assertions needs to be injected into the app/framework target directly just before running unit tests, which is not very practical when running those locally or on CI server. – i4niac Jul 10 '16 at 07:24
  • I tried to make this code reusable, to be able to plug it in as cocoapod, but the requirement to have overriding functions as part of main app/framework target is very limiting, especially when I have to scale up to 10+ frameworks. Not sure if the end result justifies the trade offs in my case. – i4niac Jul 10 '16 at 07:25
  • You are kind right. Currently, the solution provided is a hack and I do discourage you to use in production. – mohamede1945 Jul 11 '16 at 09:39
7

Nimble ("A Matcher Framework for Swift and Objective-C") got your back :

Swift Assertions

If you're using Swift, you can use the throwAssertion matcher to check if an assertion is thrown (e.g. fatalError()). This is made possible by @mattgallagher's CwlPreconditionTesting library.

// Swift

// Passes if 'somethingThatThrows()' throws an assertion, 
// such as by calling 'fatalError()' or if a precondition fails:
expect { try somethingThatThrows() }.to(throwAssertion())
expect { () -> Void in fatalError() }.to(throwAssertion())
expect { precondition(false) }.to(throwAssertion())

// Passes if throwing an NSError is not equal to throwing an assertion:
expect { throw NSError(domain: "test", code: 0, userInfo: nil) }.toNot(throwAssertion())

// Passes if the code after the precondition check is not run:
var reachedPoint1 = false
var reachedPoint2 = false
expect {
    reachedPoint1 = true
    precondition(false, "condition message")
    reachedPoint2 = true
}.to(throwAssertion())

expect(reachedPoint1) == true
expect(reachedPoint2) == false

Notes:

  • This feature is only available in Swift.
  • It is only supported for x86_64 binaries, meaning you cannot run this matcher on iOS devices, only simulators.
  • The tvOS simulator is supported, but using a different mechanism, requiring you to turn off the Debug executable scheme setting for your tvOS scheme's Test configuration.
Axel Guilmin
  • 11,454
  • 9
  • 54
  • 64
3

SWIFT 5, 4

This version not leaves a discarded thread in GCD for each call to expectFatalError. This fixed by using a Thread rather than DispatchQueue. Thanks to @jedwidz

import Foundation

// overrides Swift global `fatalError`
func fatalError(_ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line) -> Never {
    FatalErrorUtil.fatalErrorClosure(message(), file, line)
}

/// Utility functions that can replace and restore the `fatalError` global function.
enum FatalErrorUtil {
    typealias FatalErrorClosureType = (String, StaticString, UInt) -> Never
    // Called by the custom implementation of `fatalError`.
    static var fatalErrorClosure: FatalErrorClosureType = defaultFatalErrorClosure
    
    // backup of the original Swift `fatalError`
    private static let defaultFatalErrorClosure: FatalErrorClosureType = { Swift.fatalError($0, file: $1, line: $2) }
    
    /// Replace the `fatalError` global function with something else.
    static func replaceFatalError(closure: @escaping FatalErrorClosureType) {
        fatalErrorClosure = closure
    }
    
    /// Restore the `fatalError` global function back to the original Swift implementation
    static func restoreFatalError() {
        fatalErrorClosure = defaultFatalErrorClosure
    }
}

import XCTest
@testable import TargetName

extension XCTestCase {
    func expectFatalError(expectedMessage: String, testcase: @escaping () -> Void) {

        // arrange
        let expectation = self.expectation(description: "expectingFatalError")
        var assertionMessage: String? = nil

        // override fatalError. This will terminate thread when fatalError is called.
        FatalErrorUtil.replaceFatalError { message, _, _ in
            DispatchQueue.main.async {
                assertionMessage = message
                expectation.fulfill()
            }
            // Terminate the current thread after expectation fulfill
            Thread.exit()
            // Since current thread was terminated this code never be executed
            fatalError("It will never be executed")
        }

        // act, perform on separate thread to be able terminate this thread after expectation fulfill
        Thread(block: testcase).start()
        
        waitForExpectations(timeout: 0.1) { _ in
            // assert
            XCTAssertEqual(assertionMessage, expectedMessage)

            // clean up
            FatalErrorUtil.restoreFatalError()
        }
    }
}

class TestCase: XCTestCase {
    func testExpectPreconditionFailure() {
        expectFatalError(expectedMessage: "boom!") {
            doSomethingThatCallsFatalError()
        }
    }
}
Andrew Hershberger
  • 4,152
  • 1
  • 25
  • 35
  • Like this answer, but replaced almost all of the application code with... `internal var triggerFatalError = Swift.fatalError`, which I call directly. – Michael Long Jan 04 '23 at 01:26