Test unitaire fatalError dans Swift
Comment implémenter un test unitaire pour un chemin de code fatalError
dans Swift?
Par exemple, j'ai le code swift suivant
func divide(x: Float, by y: Float) -> Float {
guard y != 0 else {
fatalError("Zero division")
}
return x / y
}
Je veux tester l'Unité du cas lorsque y = 0.
Remarque, je veux utiliser fatalError pas toute autre fonction d'assertion.
3 réponses
L'idée est de remplacer la fonction fatalError
intégrée par la vôtre, qui est remplacée lors de l'exécution d'un test unitaire, de sorte que vous y exécutez des assertions de test unitaire.
Cependant, la partie délicate est que fatalError
est @noreturn
, vous devez donc le remplacer par une fonction qui ne retourne jamais.
Remplacer fatalError
Dans votre cible d'application uniquement (ne pas ajouter à la cible de test unitaire):
// 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
Ajoutez l'extension suivante à votre test unitaire cible:
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()
}
}
}
J'ai eu l'idée de ce post sur les tests unitaires assert
et precondition
:
assertion de test dans Swift
Grâce à nschum et Ken Ko pour l'idée derrière cette réponse.
Voici un aperçu de la façon de le faire.
Cette réponse n'est pas seulement pour une erreur fatale. C'est aussi pour les autres méthodes d'assertion (assert
, assertionFailure
, precondition
, preconditionFailure
et fatalError
)
1. Déposez ProgrammerAssertions.swift
sur la cible de votre application ou infrastructure testée. Juste à côté de votre 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. Déposez XCTestCase+ProgrammerAssertions.swift
sur votre cible de test. Juste à côté de vos cas de test.
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. Utilisation assert
, assertionFailure
, precondition
, preconditionFailure
et fatalError
, normalement, comme vous le faites toujours.
Par exemple: si vous avez une fonction qui fait une division comme la suivante:
func divideFatalError(x: Float, by y: Float) -> Float {
guard y != 0 else {
fatalError("Zero division")
}
return x / y
}
4. Unité de test avec les nouvelles méthodes de expectAssert
, expectAssertionFailure
, expectPrecondition
, expectPreconditionFailure
et expectFatalError
.
Vous pouvez tester le 0 division avec le code suivant.
func testFatalCorrectMessage() {
expectFatalError("Zero division") {
divideFatalError(1, by: 0)
}
}
Ou si vous ne voulez pas tester le message, vous le faites simplement.
func testFatalErrorNoMessage() {
expectFatalError() {
divideFatalError(1, by: 0)
}
}
Swift 4 et Swift 3
Basé sur la réponse de Ken.
Dans votre cible D'application ajoutez ce qui suit:
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
}
}
Dans votre cible de test ajoutez ce qui suit:
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()
}
}
}
Cas de Test:
class TestCase: XCTestCase {
func testExpectPreconditionFailure() {
expectFatalError(expectedMessage: "boom!") {
doSomethingThatCallsFatalError()
}
}
}