Проверка утверждения в Swift

Я пишу модульные тесты для метода, который имеет утверждение. Руководство Swift Language рекомендует использовать утверждения для "недопустимых условий":

Утверждения приводят к прекращению работы вашего приложения и не заменяют разработку кода таким образом, что вряд ли возникнут недопустимые условия. Тем не менее, в ситуациях, когда возможны недопустимые условия, утверждение является эффективным способом обеспечения того, чтобы такие условия были выделены и замечены во время разработки, прежде чем ваше приложение будет опубликовано.

Я хочу проверить случай неудачи.

Тем не менее, нет XCTAssertThrows в Свифте (по состоянию на бета 6). Как я могу написать модульный тест, который проверяет, что утверждение не выполняется?

редактировать

Согласно предложению @RobNapier, я попробовал обернуть XCTAssertThrows в методе Objective-C и вызове этого метода из Swift. Это не работает, так как макрос не улавливает фатальную ошибку, вызванную assertи, следовательно, тест вылетает.

6 ответов

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 Вот.

Согласитесь с комментарием nschum, что он не подходит для юнит-теста assert потому что по умолчанию это не будет в коде продукта. Но если вы действительно хотели это сделать, вот 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
    }
}

Спасибо nschum и Ken Ko за идею этого ответа.

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

Вот пример проекта

Этот ответ не только для утверждения. Это также для других методов утверждения (assert, assertionFailure, precondition, preconditionFailure а также 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. Используйте 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)
    }
}

Проект Мэтта Галлахера CwlPreconditionTesting на github добавляет catchBadInstruction функция, которая дает вам возможность проверить на наличие ошибок утверждения / предварительного условия в коде модульного тестирования.

Файл CwlCatchBadInstructionTests показывает простую иллюстрацию его использования. (Обратите внимание, что он работает только в симуляторе для iOS.)

Я полагаю, что с Beta6 для Swift все еще невозможно поймать исключение напрямую. Единственный способ справиться с этим - написать конкретный контрольный пример в ObjC.

Тем не менее, обратите внимание, что _XCTAssertionType.Throws существует, что говорит о том, что команда Swift знает об этом и собирается в конечном итоге найти решение. Вполне возможно, что вы могли бы написать это утверждение в ObjC и представить его Свифту (я не могу придумать ни одной причины, которая была бы невозможна в Beta6). Одна большая проблема заключается в том, что вы не сможете легко получить из него точную информацию о местоположении (например, конкретную строку, которая не удалась).

У нас есть код Swift (4), который тестирует среду Objective-C. Некоторые из базовых методов вызывают в 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
}
Другие вопросы по тегам