web-dev-qa-db-ja.com

Swift

アサーションを持つメソッドの単体テストを書いています。 Swift言語ガイドでは、「無効な条件」にアサーションを使用することを推奨しています。

アサーションはアプリを終了させ、無効な条件が発生する可能性が低いような方法でコードを設計する代わりにはなりません。それでも、無効な条件が発生する可能性がある状況では、アサーションは、アプリが公開される前に、開発中にそのような条件が強調表示され、通知されるようにするための効果的な方法です。

失敗事例をテストしたい。

ただし、Swift(Beta 6以降)にはXCTAssertThrowsはありません。アサーションが失敗したことをテストする単体テストを作成方法を教えてください。

編集

@RobNapierの提案に従って、Objective-CメソッドでXCTAssertThrowsをラップし、Swiftからこのメソッドを呼び出してみました。マクロはassertによって引き起こされた致命的なエラーをキャッチしないため、これは機能せず、テストがクラッシュします。

25
hpique

デフォルトではprodコードに含まれないため、assertをユニットテストするのは正しくないように思われるというnschumのコメントに同意します。しかし、本当にやりたいのであれば、参考のためにassertバージョンを次に示します。

オーバーライドアサート

func assert(@autoclosure condition: () -> Bool, @autoclosure _ message: () -> String = "", file: StaticString = __FILE__, line: UInt = __LINE__) {
    assertClosure(condition(), message(), file, line)
}
var assertClosure: (Bool, String, StaticString, UInt) -> () = defaultAssertClosure
let defaultAssertClosure = {Swift.assert($0, $1, file: $2, line: $3)}

ヘルパー拡張

extension XCTestCase {

    func expectAssertFail(expectedMessage: String, testcase: () -> Void) {
        // arrange
        var wasCalled = false
        var assertionCondition: Bool? = nil
        var assertionMessage: String? = nil
        assertClosure = { condition, message, _, _ in
            assertionCondition = condition
            assertionMessage = message
            wasCalled = true
        }

        // act
        testcase()

        // assert
        XCTAssertTrue(wasCalled, "assert() was never called")
        XCTAssertFalse(assertionCondition!, "Expected false to be passed to the assert")
        XCTAssertEqual(assertionMessage, expectedMessage)

        // clean up
        assertClosure = defaultAssertClosure
    }
}
5
Ken Ko

assertとその兄弟であるpreconditionは、例外をスローしないでください(Swift 2のエラー処理があっても))。

使用できるトリックは、同じことを行うがテスト用に置き換えることができる独自のドロップイン置換を作成することです。 (パフォーマンスが心配な場合は、#ifdefリリースビルド用に削除します。)

カスタム前提条件

/// Our custom drop-in replacement `precondition`.
///
/// This will call Swift's `precondition` by default (and terminate the program).
/// But it can be changed at runtime to be tested instead of terminating.
func precondition(@autoclosure condition: () -> Bool, @autoclosure _ message: () -> String = "", file: StaticString = __FILE__, line: UWord = __LINE__) {
    preconditionClosure(condition(), message(), file, line)
}

/// The actual function called by our custom `precondition`.
var preconditionClosure: (Bool, String, StaticString, UWord) -> () = defaultPreconditionClosure
let defaultPreconditionClosure = {Swift.precondition($0, $1, file: $2, line: $3)}

テストヘルパー

import XCTest

extension XCTestCase {
    func expectingPreconditionFailure(expectedMessage: String, @noescape block: () -> ()) {

        let expectation = expectationWithDescription("failing precondition")

        // Overwrite `precondition` with something that doesn't terminate but verifies it happened.
        preconditionClosure = {
            (condition, message, file, line) in
            if !condition {
                expectation.fulfill()
                XCTAssertEqual(message, expectedMessage, "precondition message didn't match", file: file.stringValue, line: line)
            }
        }

        // Call code.
        block();

        // Verify precondition "failed".
        waitForExpectationsWithTimeout(0.0, handler: nil)

        // Reset precondition.
        preconditionClosure = defaultPreconditionClosure
    }
}

func doSomething() {
    precondition(false, "just not true")
}

class TestCase: XCTestCase {
    func testExpectPreconditionFailure() {
        expectingPreconditionFailure("just not true") {
            doSomething();
        }
    }
}

要点

もちろん、同様のコードはassertでも機能します。ただし、動作をテストしているので、明らかにインターフェイスコントラクトの一部にする必要があります。最適化されたコードがそれに違反することを望まない場合、assertは最適化されます。したがって、ここではpreconditionを使用することをお勧めします。

5
nschum

この答えの背後にあるアイデアを提供してくれた 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)
    }
}
4
mohamede1945

Matt Gallagherの githubのCwlPreconditionTestingプロジェクトcatchBadInstruction関数を追加して、ユニットテストコードでアサーション/前提条件の失敗をテストする機能を提供します。

CwlCatchBadInstructionTestsファイル は、その使用法の簡単な図を示しています。 (iOS用のシミュレーターでのみ機能することに注意してください。)

2
user2067021

Beta6の時点では、Swiftが例外を直接キャッチすることはまだ不可能だと思います。これを処理できる唯一の方法は、その特定のテストケースをObjCで作成することです。

とはいえ、_XCTAssertionType.Throwsが存在します。これは、Swiftチームがこれを認識しており、最終的に解決策を提供する予定であることを示しています。このアサーションをObjCで自分で記述し、=に公開できることは想像に難くありません。 Swift(Beta6では不可能な理由は考えられません)1つの大きな問題は、そこから適切な位置情報を簡単に取得できない可能性があることです(特定の行たとえば、失敗しました)。

1
Rob Napier

Objective-CフレームワークをテストするSwift(4)コードがあります。フレームワークメソッドのいくつかはNSAssertを呼び出します。

NSHipster に触発されて、私は次のような実装になりました:

SwiftAssertionHandler.h(これをブリッジヘッダーで使用します)

@interface SwiftAssertionHandler : NSAssertionHandler

@property (nonatomic, copy, nullable) void (^handler)(void);

@end

SwiftAssertionHandler.m

@implementation SwiftAssertionHandler

- (instancetype)init {
    if (self = [super init]) {
        [[[NSThread currentThread] threadDictionary] setValue:self
                                                           forKey:NSAssertionHandlerKey];
    }
    return self;
}

- (void)dealloc {
    [[[NSThread currentThread] threadDictionary] removeObjectForKey:NSAssertionHandlerKey];
}

- (void)handleFailureInMethod:(SEL)selector object:(id)object file:(NSString *)fileName lineNumber:(NSInteger)line description:(NSString *)format, ... {
    if (self.handler) {
        self.handler();
    }
}

- (void)handleFailureInFunction:(NSString *)functionName file:(NSString *)fileName lineNumber:(NSInteger)line description:(NSString *)format, ... {
    if (self.handler) {
        self.handler();
    }
}

@end

Test.Swift

let assertionHandler = SwiftAssertionHandler()
assertionHandler.handler = { () -> () in
    // i.e. count number of assert
}
0
Mr_Pouet