web-dev-qa-db-ja.com

ユニットテストのfatalError Swift

SwiftでfatalErrorコードパスの単体テストを実装する方法

たとえば、私は次のようにしていますSwiftコード

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

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

    return x / y
}

Y = 0の場合のユニットテストを行いたい。

他のアサーション関数ではなく、fatalErrorを使用したいことに注意してください。

30
mohamede1945

組み込みのfatalError関数を独自の関数に置き換え、ユニットテストの実行中に置き換えられるようにして、その中でユニットテストアサーションを実行します。

ただし、トリッキーな部分はfatalError@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($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 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()
        }
    }
}

この投稿から、ユニットテストassertおよびpreconditionについてのアイデアを得ました: Testing assertion in Swift

18
Ken Ko

Swift 4およびSwift 3

ケンの答えに基づいています。

アプリターゲット内以下を追加します。

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

テストターゲット内以下を追加します。

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()
        }
    }
}
14
Guy Daher

nschumKen Ko に感謝します。

これを行う方法の要点は次のとおりです。

これはサンプルプロジェクトです

この答えは致命的なエラーだけではありません。他のアサーションメソッド(assertassertionFailurepreconditionpreconditionFailureおよびfatalError)にも使用できます

1.ドロップProgrammerAssertions.Swiftテスト対象のアプリまたはフレームワークのターゲットに。あなたのソースコードに加えて。

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をテストターゲットに追加します。あなたのテストケース以外にも。

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.いつものように、assertassertionFailurepreconditionpreconditionFailureおよびfatalErrorを通常どおり使用します。

たとえば、次のような除算を行う関数があるとします。

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

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

    return x / y
}

4.新しいメソッドexpectAssertexpectAssertionFailureexpectPreconditionexpectPreconditionFailureおよびexpectFatalErrorを使用してユニットテストを行います。

次のコードで0除算をテストできます。

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

または、メッセージをテストしたくない場合は、単にテストします。

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