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:
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.
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
)
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)
}
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)
}
}
}
}
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
}
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)
}
}
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()
}
}
}
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.
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
}
}
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()
}
}
}
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