【发布时间】: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)
}
但是,我不确定拨打expression1 和expression2 的正确方法。
如果我尝试以下操作,我会得到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()
}
}
}
【问题讨论】: