swift unit-testing fatal-error

Prueba de unidad fatalError in Swift



unit-testing fatal-error (3)

¿Cómo implementar la prueba unitaria para una fatalError código de fatalError en Swift?

Por ejemplo, tengo el siguiente código swift

func divide(x: Float, by y: Float) -> Float { guard y != 0 else { fatalError("Zero division") } return x / y }

Quiero hacer una prueba unitaria del caso cuando y = 0.

Tenga en cuenta que quiero utilizar fatalError no ninguna otra función de aserción.


Swift 4 y Swift 3

Basado en la respuesta de Ken.

En tu App Target agrega lo siguiente:

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 } }

En su objetivo de prueba agregue lo siguiente:

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

Caso de prueba:

class TestCase: XCTestCase { func testExpectPreconditionFailure() { expectFatalError(expectedMessage: "boom!") { doSomethingThatCallsFatalError() } } }


Gracias a nschum y por la idea detrás de esta respuesta.

Aquí hay una idea de cómo hacerlo.

Aquí hay un proyecto de ejemplo.

Esta respuesta no es solo por error fatal. También es para los otros métodos de aserción ( assertionFailure , assertionFailure , precondition , preconditionFailure precondition , fatalError y fatalError )

1. Coloque ProgrammerAssertions.swift en el destino de su aplicación o marco bajo prueba. Justo al lado de su código fuente.

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. XCTestCase+ProgrammerAssertions.swift en su objetivo de prueba. Justo aparte de tus casos de prueba.

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 y fatalError normalmente como siempre hace.

Por ejemplo: si tiene una función que hace una división como la siguiente:

func divideFatalError(x: Float, by y: Float) -> Float { guard y != 0 else { fatalError("Zero division") } return x / y }

4. expectAssert prueba de unidad con los nuevos métodos expectAssert , expectAssertionFailure , expectPrecondition , expectPreconditionFailure y expectFatalError .

Puedes probar la división 0 con el siguiente código.

func testFatalCorrectMessage() { expectFatalError("Zero division") { divideFatalError(1, by: 0) } }

O si no quieres probar el mensaje, simplemente lo haces.

func testFatalErrorNoMessage() { expectFatalError() { divideFatalError(1, by: 0) } }


La idea es reemplazar la función fatalError incorporada con la suya propia, que se reemplaza durante la ejecución de una prueba unitaria, para que pueda ejecutar las aserciones de la prueba unitaria.

Sin embargo, la parte difícil es que fatalError es @noreturn , por lo que debe anularlo con una función que nunca devuelve.

Anular fatalError

Solo en su objetivo de aplicación (no agregue al objetivo de prueba de unidad):

// 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 } }

Extensión

Agregue la siguiente extensión a su objetivo de prueba de unidad:

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

Caso de prueba

class TestCase: XCTestCase { func testExpectPreconditionFailure() { expectFatalError("boom!") { doSomethingThatCallsFatalError() } } }

A partir de este post, obtuve la idea sobre la afirmación y la precondition pruebas de unidad: Afirmación de prueba en Swift