【问题标题】:Unit test that verifies runtime error is thrown in Swift验证运行时错误的单元测试在 Swift 中抛出
【发布时间】:2020-10-28 00:54:12
【问题描述】:

谷歌搜索让我找到了通用的 Swift 错误处理链接,但我有一个更具体的问题。我认为这里的答案是“不,你不走运”,但我想仔细检查一下我是否遗漏了什么。 This 几年前的问题似乎很相似,并且有一些看起来很粗糙的解决方法的答案......我正在寻找最新版本的 Swift 是否可以让一些更优雅的事情成为可能。

情况:我有一个没有用throws标记的函数,使用try!

目标:我想创建一个单元测试来验证,是的,给这个函数一个错误的东西实际上会失败并抛出一个(运行时/致命)错误。

问题:

  1. 当我将此函数包装在 do-catch 中时,编译器会警告我无法访问 catch 块。
  2. 当我运行测试并传入错误参数时,do-catch 不会捕获错误。
  3. XCTAssertThrows 也没有捕捉到错误。

此函数被构建为与另一个静默失败的函数具有相同的签名,我在模拟器上将它换成这个函数,这样我就可以在测试期间大声失败(无论是自动的还是手动的)。所以我不能把它改成一个 throwing 函数,因为那样的话另一个函数必须被标记为 throwing 并且我希望它静默失败。

那么,有没有办法抛出一个我可以在单元测试中捕获的未处理错误?

或者,我可以在不更改签名的情况下以可测试的方式使此函数爆炸吗?

【问题讨论】:

  • 也许这是当前 Swift 设计无法实现的?让我开始赏金,看看我们是否可以获得任何额外的输入。
  • 你能显示被测代码吗? (未标记throws的函数,使用try!。)
  • 你说:I want it to fail silently是什么意思?你想让应用程序崩溃吗?还是只是阻止代码执行?

标签: swift error-handling try-catch swift5


【解决方案1】:

没有办法在 swift 中捕获 non-throwing 错误,您的意思是在 try 之后使用 !

但你可以重构你的代码,让你可以从函数外部获得更多控制,如下所示:

  1. 将 throwing 函数分解出来,以便您以正确的方式对其进行测试:
func throwingFunc() throws {
    let json = "catch me if you can".data(using: .utf8)!
    try JSONDecoder().decode(Int.self, from: json)
}
  1. 使用自定义错误处理程序编写非抛出包装器:
func nonThrowingFunc( catchHandler:((Error)->Void)? = nil ) {
    guard let handler = catchHandler else { return try! throwingFunc() }
    do {
        try throwingFunc()
    } catch {
        handler(error)
    }
}

所以只有当你正在处理它时才会调用处理程序:

// Test the function and faild the test if needed
nonThrowingFunc { error in
    XCTFail(error.localizedDescription)
}

你有一个崩溃的:

// Crash the program
nonThrowingFunc() 

注意

!(作为强制)专为您非常确定结果的情况而设计。很好的例子:

  • 解码硬编码或静态 JSON
  • 强制展开硬编码值
  • 当你知道接口的实现是什么时强制尝试接口

如果您的函数不够纯,并且可能因传递不同的参数而失败,您应该考虑强制它并将您的代码重构为更安全的版本。

【讨论】:

  • 正如我在问题中所说,这个单元测试是为了验证代码是否仅在特定情况下崩溃。也就是说,我正在编写在生产中静默失败的代码,处理错误,但在模拟器上我希望它大声失败,以便我可以在发布之前捕获错误。但是,是的,您是正确的,Apple 认为这种情况不值得支持。
【解决方案2】:

Swift(直到 & 包括当前 5.5)明确假设 inside non-throwing 函数 必须 处理 inside(!) 的所有错误。所以 swift 的设计没有公共机制来干预这个过程并产生运行时错误。

【讨论】:

    【解决方案3】:

    你可能甚至不喜欢我的回答。但它是这样的:

    虽然我同意在很多情况下try!try 更有用(或事件更好),但我也认为它们不应该被测试,因为它们表示程序员的错误。程序员的错误是计划外的。你不能测试计划外的东西。如果您预计(或怀疑)生产中会发生错误,那么您首先不应该使用try!。对我来说,这违反了我认为的编程标准。

    Throwable 被添加来处理预期的错误,并使用try! 告诉编译器你预期的错误永远不会发生。例如,当您解析一个您知道永远不会失败的硬编码值时。那么,为什么您需要测试一个永远不会发生的错误呢?

    如果您希望在调试时严格但在发布时安全,您也可以使用 assertionFailure。

    【讨论】:

      【解决方案4】:

      运行时错误,例如

      • 数组索引超出范围
      • 强制解开 nil 值
      • 除以零

      ,被认为是编程错误,需要进行前置条件检查以减少它们在生产中发生的机会。前置条件失败通常在测试阶段发现,因为 QA 人员将应用程序从各个方面折腾,试图找出实现缺陷。

      由于它们是编程错误,因此您不需要对其进行测试,如果代码的执行确实达到了断言失败的程度,则意味着其他代码未能提供有效数据。这就是您应该对其他代码进行单元测试的内容。

      最好避免使用断言。 !, try!, IOU's, 都是 nil 值的陷阱,所以如果你不能 100% 确定你只会收到非 nil 值,最好避免这些构造。

      大多数情况下,编程错误意味着您的应用程序进入了不可恢复的状态,之后几乎无能为力,所以最好让它崩溃,然后继续进入无效状态。

      因此,Swift 不需要公开 API 来处理这种情况。现有的解决方法复杂、脆弱,不值得在实际应用中使用。

      总结:

      • 用可以处理 nil 的代码替换强制解包(包括try!),并对其进行单元测试,或者,
      • 对调用者代码进行单元测试,因为那是实际有问题的代码。

      后一种情况假定强制解包使用是合法的,因为代码期望被调用者不为零,因此负担转移到了另一段代码,即被强制解包的值的提供者。

      【讨论】:

      • 正如我在问题中所说,这个单元测试是为了验证代码是否仅在特定情况下崩溃。也就是说,我正在编写在生产中静默失败的代码,处理错误,但在模拟器上我希望它大声失败,以便我可以在发布之前捕获错误。但是,是的,您是正确的,Apple 认为这种情况不值得支持。
      • @ElliotSchrock 我的观点是,最好从一开始就避免强制展开,例如 !try!。唯一有意义的是使用它们,如果你 100% 确定你永远不会得到 nil 值,并且在这种情况下对该代码进行单元测试不会增加太多价值。
      【解决方案5】:

      在 swift 中使用通用函数 XCTAssertThrowsError 进行单元测试

      断言表达式引发错误。

      func XCTAssertThrowsError<T>(_ expression: @autoclosure () throws -> T, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line, _ errorHandler: (_ error: Error) -> Void = { _ in })
      

      https://developer.apple.com/documentation/xctest/1500795-xctassertthrowserror

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-08-19
        • 1970-01-01
        • 2018-11-09
        • 1970-01-01
        • 1970-01-01
        • 2017-07-21
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多