1

I'm creating a custom assertion with accuracy. I copied Apple's XCTAssertEqual method signature:

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

I tried creating my own with a custom type, making sure to forward the file and line numbers:

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

However, I'm not sure on the proper way to call expression1 and expression2.

If I try the following, I get Call can throw, but it is not marked with 'try' and the error is not handled:

let value1 = expression1()

If I try adding try, I get Errors thrown from here are not handled:

let value1 = try expression1()

Obviously I could add try! but it seems like it would crash ungracefully in the XCTestCase (e.g. immediate crash rather than having the individual test fail).

try? seems like it would hide the crash.

Another option is to inline the function calls (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)

However, this means that the expression is called multiple times. If it is time consuming or not idempotent, this could cause issues when evaluated multiple times.


What's the proper way to call these throwing autoclosures when defining a custom XCTAssert?


Update: I tried the rethrows method, but it doesn't behave the same as XCTAssertEqual. Namely, it requires that I add try in front of my assertion method, whereas XCTAssert never has that requirement.

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

Notice how Apple's XCTAssertEqual(true, try exceptionMethod()) compiles just fine, but how XCTMyAssertEqualRethrows(true, try exceptionMethod()) does not compile.

The fact that it does not compile, along with the omission of rethrows in Apple's signature for XCTAssertEqual, leads me to believe that adding rethrows is not the proper solution for this.


Update2: One way to get it to function very similarly to Apple is to explicitly catch the exception. However, this seems heavy-handed and like there is probably a better alternative. To demonstrate this solution, as well as most of the other solutions mentioned, here is a self-contained code snippet:

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

Senseful
  • 86,719
  • 67
  • 308
  • 465

2 Answers2

1

You just need to mark your function as rethrows and then call the throwing expressions with try.

rethrows tells the compiler that the function only ever throws error thrown by one of its throwing input arguments. It never throws errors on its own. For more on rethrows, check this excellent answer.

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()
    ...
}
Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
  • Thanks for the suggestion. Unfortunately, adding `rethrows` causes the assertion method to no longer compile as-is. Instead, you must add a `try` at the call-site of the assertion method itself. With Apple's `XCTAssertEqual`, I do not have such a restriction, leading me to believe there is a better option than `rethrows`. I updated the question with this info. – Senseful Aug 05 '20 at 19:29
0

Declare your helper as throws, then add the try to your expressions.

Then in your tests, call your helper with try and declare the tests as throws.

Jon Reid
  • 20,545
  • 2
  • 64
  • 95