Модульный тест fatalError в Swift

Как реализовать модульный тест для fatalError путь кода в Swift?

например, у меня есть следующий swift-код

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

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

    return x / y
}

Я хочу проверить случай, когда y = 0.

примечание, Я хочу использовать fatalError не любую другую функцию утверждения.

3 ответов


идея в том, чтобы заменить встроенный fatalError функция с вашим собственным, который заменяется во время выполнения модульного теста, так что вы запустите утверждения модульного теста в нем.

однако, сложная часть заключается в том, что fatalError is @noreturn, поэтому вам нужно переопределить его функцией, которая никогда не возвращается.

переопределить fatalError

только в вашем приложении (не добавляйте в цель модульного теста):

// 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(, file: , line: ) }

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

я получил идею из этого поста о unit тестирования assert и precondition: тестирование утверждения в Swift


спасибо nschum и Кэн Ко по идее этот ответ.

вот суть того, как это сделать.

вот пример проекта.

этот ответ не только для фатальной ошибки. Это также для других методов утверждения (assert, assertionFailure, precondition, preconditionFailure и fatalError)

1. Падение ProgrammerAssertions.swift к цели вашего приложения или тестируемой платформы. Просто кроме исходного кода.

ProgrammerAssertions.Свифт!--27-->

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(, , file: , line: ) }
    public static let swiftAssertionFailureClosure    = { Swift.assertionFailure(, file: , line: ) }
    public static let swiftPreconditionClosure        = { Swift.precondition(, , file: , line: ) }
    public static let swiftPreconditionFailureClosure = { Swift.preconditionFailure(, file: , line: ) }
    public static let swiftFatalErrorClosure          = { Swift.fatalError(, file: , line: ) }
}

/// 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 к вашей тестовой цели. Кроме ваших тестовых случаев.

XCTestCase + ProgrammerAssertions.Свифт!--27-->

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. Использовать assert, assertionFailure, precondition, preconditionFailure и fatalError обычно, как вы всегда делаете.

например: если у вас есть функция, которая выполняет подразделение, так:

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

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

    return x / y
}

4. Блок-тест их с новым методы expectAssert, expectAssertionFailure, expectPrecondition, expectPreconditionFailure и expectFatalError.

вы можете проверить деление 0 со следующим кодом.

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

или если вы не хотите тестировать сообщение, вы просто делаете.

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

Swift 4 и Swift 3

на основе ответа Кена.

в вашем приложении Target добавить следующее:

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(, file: , line: ) }

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

в вашей тестовой цели добавить следующее:

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

тест:

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