Как обрабатывать "броски" в пользовательской функции XCTAssert? - PullRequest
1 голос
/ 05 августа 2020

Я создаю настраиваемое утверждение с точностью. Я скопировал подпись метода Apple 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!, но похоже, что sh неуклюже в XCTestCase (например, немедленный cra sh вместо того, чтобы иметь отдельный тест не пройден).

try? кажется, что это скроет трещину sh.

Другой вариант - встроить вызовы функций (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?

Обновление: I попробовал rethrows метод , но он не работает так же, как 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()) нет. compile.

Тот факт, что он не компилируется, наряду с отсутствием rethrows в подписи Apple для XCTAssertEqual, заставляет меня думать, что добавление rethrows не является подходящим решением для этого.

Update2: Один из способов заставить его работать, очень похожий на Apple, - это явно перехватить исключение. Однако это кажется деспотичным и, вероятно, есть лучшая альтернатива. Чтобы продемонстрировать это решение, а также большинство других упомянутых решений, вот автономный фрагмент кода:

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

Ответы [ 2 ]

1 голос
/ 05 августа 2020

Вам просто нужно пометить вашу функцию как rethrows, а затем вызвать выражения бросания с помощью try.

rethrows сообщает компилятору, что функция выдает только ошибку, вызванную одним из ее бросков входные аргументы. Он никогда не выдает ошибок сам по себе. Чтобы узнать больше о rethrows, отметьте этот отличный ответ.

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()
    ...
}
0 голосов
/ 07 августа 2020

Объявите своего помощника как throws, затем добавьте try в свои выражения.

Затем в своих тестах вызовите своего помощника с помощью try и объявите тесты как throws.

...