【问题标题】:How to handle 'throws' in custom XCTAssert function?如何处理自定义 XCTAssert 函数中的“抛出”?
【发布时间】:2020-11-25 20:46:15
【问题描述】:

我正在准确地创建custom assertion。我复制了苹果的XCTAssertEqual方法签名:

public func XCTAssertEqual<T>(
  _ expression1: @autoclosure () throws -> T, 
  _ expression2: @autoclosure () throws -> T,
  accuracy: T,
  _ message: @autoclosure () -> String = "",
  file: StaticString = #filePath,
  line: UInt = #line) where T : FloatingPoint

我尝试使用自定义类型创建自己的,确保转发文件和行号:

  func XCTAssertEqualUser(
    _ expression1: @autoclosure () throws -> User,
    _ expression2: @autoclosure () throws -> User,
    accuracy: Double,
    _ message: @autoclosure () -> String = "",
    file: StaticString = #filePath,
    line: UInt = #line
  ) {
    let value1 = expression1() // ❌ Call can throw, but it is not marked with 'try' and the error is not handled
    let value2 = expression2() // ❌ Call can throw, but it is not marked with 'try' and the error is not handled

    XCTAssertEqual(value1.name, value2.name, message(), file: file, line: line)
    XCTAssertEqual(value1.age, value2.age, accuracy: accuracy, message(), file: file, line: line)
  }

但是,我不确定拨打expression1expression2 的正确方法。

如果我尝试以下操作,我会得到Call can throw, but it is not marked with 'try' and the error is not handled

let value1 = expression1()

如果我尝试添加try,我会得到Errors thrown from here are not handled

let value1 = try expression1()

显然我可以添加try!,但它似乎会在 XCTestCase 中不正常地崩溃(例如,立即崩溃而不是单个测试失败)。

try? 似乎可以隐藏崩溃。

另一种选择是内联函数调用 (test2):

    XCTAssertEqual(try expression1().name, try expression2().name, message(), file: file, line: line)
    XCTAssertEqual(try expression1().age, try expression2().age, accuracy: accuracy, message(), file: file, line: line)

但是,这意味着该表达式被多次调用。如果耗时或非幂等,则在多次评估时可能会导致问题。


在定义自定义 XCTAssert 时调用这些抛出自动关闭的正确方法是什么?


更新:tried the rethrows method,但它的行为与XCTAssertEqual 不同。也就是说,它要求我在我的断言方法前面添加try,而 XCTAssert 从来没有这个要求。

class MyTests: XCTestCase {
  func test1() {
    XCTAssertEqual(true, true)
    XCTAssertEqual(true, try exceptionMethod()) // Works as expected: XCTAssertEqual failed: threw error "Err()"
    XCTAssertThrowsError(try exceptionMethod())
    XCTAssertNoThrow(try exceptionMethod()) // Works as expected: XCTAssertNoThrow failed: threw error "Err()"
  }

  func test2() {
    XCTMyAssertEqualRethrows(true, true)
    //XCTMyAssertEqualRethrows(true, try exceptionMethod()) // ❌ Does not compile: Call can throw, but it is not marked with 'try' and the error is not handled
  }
}

func XCTMyAssertEqualRethrows(
  _ expression1: @autoclosure () throws -> Bool,
  _ expression2: @autoclosure () throws -> Bool,
  file: StaticString = #filePath,
  line: UInt = #line
) rethrows {
  let value1 = try expression1()
  let value2 = try expression2()

  XCTAssertEqual(value1, value2, file: file, line: line)
  XCTAssertThrowsError(value2, file: file, line: line)
  XCTAssertNoThrow(value2, file: file, line: line)
}

func exceptionMethod() throws -> Bool {
  struct Err: Error { }
  throw Err()
}

注意 Apple 的 XCTAssertEqual(true, try exceptionMethod()) 编译得很好,但 XCTMyAssertEqualRethrows(true, try exceptionMethod()) 没有编译。

它无法编译的事实,以及 Apple 对 XCTAssertEqual 的签名中省略了 rethrows,让我相信添加 rethrows 并不是解决此问题的正确方法。


更新 2: 使其功能与 Apple 非常相似的一种方法是显式捕获异常。但是,这似乎很严厉,并且可能有更好的选择。为了演示这个解决方案,以及提到的大多数其他解决方案,这里有一个自包含的代码 sn-p:

class MyTests: XCTestCase {
  func test() {
    // Apple default. Notice how this compiles nicely with both non-throwing functions and throwing
    // functions. This is the ideally how any solution should behave.
    XCTAssertEqual(User().name, User().name)
    XCTAssertEqual(User().age, User().age, accuracy: 0.5)

    XCTAssertEqual(try User(raiseError: true).name, User().name) // XCTAssertEqual failed: threw error "Err()"
    XCTAssertEqual(try User(raiseError: true).age, User().age, accuracy: 0.5) // XCTAssertEqual failed: threw error "Err()"
  }

  func test2() {
    // This solution wraps Apple's assertions in a custom-defined method, and makes sure to forward
    // the file and line number. By adding `try` to each expression, it functions exactly as expected.
    //
    // The problem is that the expressions are evaluated multiple times. If they are not idempotent
    // or they are time-consuming, this could lead to problems.
    XCTAssertEqualUser2(User(), User(), accuracy: 0.5)
    XCTAssertEqualUser2(try User(raiseError: true), User(), accuracy: 0.5) // XCTAssertEqual failed: threw error "Err()"
  }

  func XCTAssertEqualUser2(
    _ expression1: @autoclosure () throws -> User,
    _ expression2: @autoclosure () throws -> User,
    accuracy: Double,
    _ message: @autoclosure () -> String = "",
    file: StaticString = #filePath,
    line: UInt = #line
  ) {
    XCTAssertEqual(try expression1().name, try expression2().name, message(), file: file, line: line)
    XCTAssertEqual(try expression1().age, try expression2().age, accuracy: accuracy, message(), file: file, line: line)
  }

  func test3() {
    // One way to fix the multiple evaluations, is to evaluate them once and marke the method as
    // rethrows.
    //
    // The problem is that this causes the second line to no longer compile.
    XCTAssertEqualUser3(User(), User(), accuracy: 0.5)
    //XCTAssertEqualUser3(try User(raiseError: true), User(), accuracy: 0.5) // ❌ Call can throw, but it is not marked with 'try' and the error is not handled
  }

  func XCTAssertEqualUser3(
    _ expression1: @autoclosure () throws -> User,
    _ expression2: @autoclosure () throws -> User,
    accuracy: Double,
    _ message: @autoclosure () -> String = "",
    file: StaticString = #filePath,
    line: UInt = #line
  ) rethrows {
    let value1 = try expression1()
    let value2 = try expression2()

    XCTAssertEqual(value1.name, value2.name, message(), file: file, line: line)
    XCTAssertEqual(value1.age, value2.age, accuracy: accuracy, message(), file: file, line: line)
  }

  func test4() {
    // By removing `rethrows` and explicitly catching the error, it compiles again.
    //
    // The problem is that this seems rather verbose. There is likely a better way to achieve a
    // similar result.
    XCTAssertEqualUser4(User(), User(), accuracy: 0.5)
    XCTAssertEqualUser4(try User(raiseError: true), User(), accuracy: 0.5) // failed - XCTAssertEqualUser4 failed: threw error "Err()"
  }

  func XCTAssertEqualUser4(
    _ expression1: @autoclosure () throws -> User,
    _ expression2: @autoclosure () throws -> User,
    accuracy: Double,
    _ message: @autoclosure () -> String = "",
    file: StaticString = #filePath,
    line: UInt = #line
  ) {
    let value1: User
    let value2: User

    do {
      value1 = try expression1()
      value2 = try expression2()
    } catch {
      XCTFail("XCTAssertEqualUser4 failed: threw error \"\(error)\"", file: file, line: line)
      return
    }

    XCTAssertEqual(value1.name, value2.name, message(), file: file, line: line)
    XCTAssertEqual(value1.age, value2.age, accuracy: accuracy, message(), file: file, line: line)
  }
}

struct User: Equatable {
  var name: String = ""
  var age: Double = 20
}

extension User {
  init(raiseError: Bool) throws {
    if raiseError {
      struct Err: Error {}
      throw Err()
    } else {
      self.init()
    }
  }
}

【问题讨论】:

    标签: swift xctest throw


    【解决方案1】:

    您只需将您的函数标记为rethrows,然后使用try 调用抛出表达式。

    rethrows 告诉编译器该函数只会抛出由其抛出的输入参数之一抛出的错误。它永远不会自己抛出错误。有关rethrows 的更多信息,请查看this 优秀答案。

    public func XCTAssertEqual<T: FloatingPoint>(
        _ expression1: @autoclosure () throws -> T,
        _ expression2: @autoclosure () throws -> T,
        accuracy: T,
        _ message: @autoclosure () -> String = "",
        file: StaticString = #file,
        line: UInt = #line) rethrows {
        let value1 = try expression1()
        let value2 = try expression2()
        ...
    }
    

    【讨论】:

    • 感谢您的建议。不幸的是,添加rethrows 会导致断言方法不再按原样编译。相反,您必须在断言方法本身的调用点添加try。使用 Apple 的XCTAssertEqual,我没有这样的限制,让我相信有比rethrows 更好的选择。我用此信息更新了问题。
    【解决方案2】:

    将您的助手声明为throws,然后将try 添加到您的表达式中。

    然后在您的测试中,使用try 调用您的助手并将测试声明为throws

    【讨论】:

      猜你喜欢
      • 2016-04-18
      • 2022-12-12
      • 2020-08-03
      • 1970-01-01
      • 2021-08-09
      • 2015-10-28
      • 2022-11-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多