【问题标题】:Testing assertion in Swift在 Swift 中测试断言
【发布时间】:2014-10-21 04:24:55
【问题描述】:

我正在为具有断言的方法编写单元测试。 Swift 语言指南建议对“无效条件”使用断言:

断言会导致您的应用终止并且不能替代 以不太可能出现无效条件的方式设计代码 出现。尽管如此,在条件无效的情况下 可能,断言是一种有效的方式来确保这样的 条件在开发过程中被突出显示,在你之前 应用已发布。

我想测试失败案例。

但是,Swift 中没有 XCTAssertThrows(从 Beta 6 开始)。如何编写单元测试来测试断言失败?

编辑

根据@RobNapier 的建议,我尝试将XCTAssertThrows 包装在一个Objective-C 方法中并从Swift 中调用该方法。这不起作用,因为宏没有捕获由assert 引起的致命错误,因此测试崩溃。

【问题讨论】:

  • 请注意,虽然断言可用于检查边界条件,但它们只能在调试模式下进行评估。发布版本不会评估断言。[1] [1]:developer.apple.com/swift/blog/?id=4

标签: unit-testing swift xctest


【解决方案1】:

我们有测试 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
}

【讨论】:

    【解决方案2】:

    Matt Gallagher 的 CwlPreconditionTesting project on github 添加了一个 catchBadInstruction 函数,使您能够在单元测试代码中测试断言/前置条件失败。

    CwlCatchBadInstructionTests file 显示了其使用的简单说明。 (请注意,它仅适用于 iOS 模拟器。)

    【讨论】:

      【解决方案3】:

      感谢 nschumKen Ko 提供此答案背后的想法。

      Here is a gist for how to do it

      Here is an example project

      这个答案不仅适用于断言。它也适用于其他断言方法(assertassertionFailurepreconditionpreconditionFailurefatalError

      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。像往常一样正常使用assertassertionFailurepreconditionpreconditionFailurefatalError

      例如:如果您有一个执行如下除法的函数:

      func divideFatalError(x: Float, by y: Float) -> Float {
      
          guard y != 0 else {
              fatalError("Zero division")
          }
      
          return x / y
      }
      

      4。使用新方法 expectAssertexpectAssertionFailureexpectPreconditionexpectPreconditionFailureexpectFatalError 对它们进行单元测试。

      您可以使用以下代码测试0除法。

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

      或者,如果您不想测试该消息,您只需这样做。

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

      【讨论】:

        【解决方案4】:

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

        【讨论】:

        • 这种方法现在有效吗?尝试测试但没有成功,从未调用过假断言,并且测试总是在代码中的真实断言上停止
        【解决方案5】:

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

        (gist)

        当然,assert 也可以使用类似的代码。但是,由于您正在测试该行为,您显然希望它成为您的接口契约的一部分。您不希望优化代码违反它,assert 将被优化掉。所以最好在这里使用precondition

        【讨论】:

          【解决方案6】:

          我相信从 Beta6 开始,Swift 仍然不可能直接捕获异常。处理这个问题的唯一方法是在 ObjC 中编写特定的测试用例。

          也就是说,请注意_XCTAssertionType.Throws 确实存在,这表明 Swift 团队已经意识到这一点,并打算最终提供解决方案。完全可以想象,您可以自己在 ObjC 中编写此断言并将其公开给 Swift(我想不出任何在 Beta6 中不可能的原因)。一个大问题是您可能无法轻易地从中获得良好的位置信息(例如,失败的特定线路)。

          【讨论】:

          • 天啊!为宏编写一个 ObjC 包装器应该可以解决问题。稍后我会确认它是否有效。
          • 看起来ObjC宏XCTAssertThrows没有捕捉到assert引起的fatal error,因此测试崩溃了。
          • 啊……有道理。我相信assert 会抛出SIGABRT,所以也许正确的答案是有一个信号处理程序。 (我假设你的意思是 assert 即使完全在 ObjC 中也不会被捕获,这听起来是对的。)
          • 在相关说明中,对于那些希望进行单元测试 fatalError 的人,我找到了基于 nschum 答案的解决方案。见stackoverflow.com/questions/32873212/…
          猜你喜欢
          • 1970-01-01
          • 2011-03-25
          • 1970-01-01
          • 1970-01-01
          • 2020-02-11
          • 2020-05-29
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多