Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit Test fatalError in Swift

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.

like image 928
mohamede1945 Avatar asked Sep 30 '15 18:09

mohamede1945


People also ask

How do I test fatalError Swift?

If you're using Swift, you can use the throwAssertion matcher to check if an assertion is thrown (e.g. fatalError()).

What is unit testing in Swift?

A unit test is a function you write that tests something about your app. A good unit test is small. It tests just one thing in isolation. For example, if your app adds up the total amount of time your user spent doing something, you might write a test to check if this total is correct.

How does Swift handle fatal errors?

According to Swift Error Handling Rationale, the general advice is: Logical error indicates that a programmer has made a mistake. It should be handled by fixing the code, not by recovering from it. Swift offers several APIs for this: assert() , precondition() and fatalError() .


2 Answers

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

like image 178
Ken Ko Avatar answered Oct 13 '22 22:10

Ken Ko


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()         }     } } 
like image 30
Guy Daher Avatar answered Oct 14 '22 00:10

Guy Daher