DEV Community

David Goyes
David Goyes

Posted on

Swift Testing #9: Manejo de errores esperados en una prueba con #expect y #require

Los macros expect(_:_:sourceLocation:) y require(_:_:sourceLocation:) pueden ser usados para validar los resultados y errores esperados.

Validar que el código arroje un error esperado

Para el siguiente análisis, considerar el siguiente código de producción:

enum SomeError: Swift.Error {
  case error, errorEsperado, otroError
}
class FailingFeature {
  var error: SomeError?
  func execute() throws(SomeError) {
    if let error {
      throw error
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Caso 1: Una función que arroja error, aunque no debería

Dada una función que potencialmente puede arrojar un error (e.g. execute() throws), se puede construir una prueba que esté marcada con throws para hacer el llamado del código de producción con try. Luego, si el código de producción arroja un error, la prueba falla.

@Test
func basicThrowingTest() throws {
  let sut = FailingFeature()
  try sut.execute() // ⚠️ Falla si arroja error
}
Enter fullscreen mode Exit fullscreen mode

Caso 2: Validar un error específico

Para verificar que el código de producción arroje un error específico, se lo puede pasar como primer argumento a expect(throws:_:sourceLocation:performing:) y luego pasarle un closure que invoque el código a probar.

@Test
func captureSpecificError() throws {
  let sut = FailingFeature()
  sut.error = .error
  #expect(throws: SomeError.error) { // ✅ Continúa aunque falle
    try sut.execute()
  }
  try #require(throws: SomeError.error) { // ❌ Falla y para
    try sut.execute()
  }
}
Enter fullscreen mode Exit fullscreen mode

Tener en cuenta que también se puede usar require(throws:_:sourceLocation:performing:), y que la diferencia radica en que expect deja que la prueba continúe, mientras que require arroja un error y provoca que la prueba pare.

Notar que el primer parámetro, tanto de la firma de #expect como #require es throws error: E lo cual indica que se espera recibir un error específico.

Caso 3: Validar un error de un tipo específico

Para validar que el código bajo prueba arroja un error de un tipo específico, se debe pasar el tipo (e.g. SomeError.self) como el primer argumento de expect(throws:_:sourceLocation:performing:) o require(throws:_:sourceLocation:performing:):

@Test
func captureSpecificErrorType() throws {
  let sut = FailingFeature()
  sut.error = .error
  #expect(throws: SomeError.self) { // ✅ Continúa aunque falle
    try sut.execute()
  }
  try #require(throws: SomeError.self) { // ❌ Falla y para
    try sut.execute()
  }
}
Enter fullscreen mode Exit fullscreen mode

Notar que el primer parámetro, tanto de la firma de #expect como #require es throws errorType: E.Type, lo cual indica que se espera recibir un TIPO de error específico.

Caso 4: Validar un error de cualquier tipo

Como extensión del caso anterior, si el tipo de error que quiero validar es cualquiera, tendría que usar (any Error).self como el primer argumento de expect(throws:_:sourceLocation:performing:) o require(throws:_:sourceLocation:performing:):

@Test
func captureAnyError() throws {
  let sut = FailingFeature()
  sut.error = .error
  #expect(throws: (any Swift.Error).self) { // ✅ Continúa aunque falle
    try sut.execute()
  }
  try #require(throws: (any Swift.Error).self) { // ❌ Falla y para
    try sut.execute()
  }
}
Enter fullscreen mode Exit fullscreen mode

Caso 5: Validar que el código no arroje error

Generalmente basta con hacer que la firma de la función de prueba arroje (se modificada con throws) para detectar que el código de producción no arroje nada. Sin embargo, en esta implementación la prueba se detiene cuando ocurre la excepción. Por esta razón, puede ser conveniente capturar el error sin detener el resto de la función de prueba. En este caso, el tipo del error esperado sería Never.

@Test
func captureNoError() {
  let sut = FailingFeature()
  #expect(throws: Never.self) { // ✅ Espera nunca (Never) recibir error
    try sut.execute()
  }
// ❌ No tiene sentido usar #require
//  try #require(throws: (any Swift.Error).self) {
//    try sut.execute()
//  }
}
Enter fullscreen mode Exit fullscreen mode

Caso 6: Inspeccionar el error arrojado

Al usar #expect(throws:) o #require(throws:), si el error concuerda con el esperado, se retorna a la función de prueba invocadora para que se pueda llevar a cabo alguna validación adicional.

Si la expectativa falla porque no hubo error, o se recibió un error diferente al esperado, entonces #expect(throws:) retorna nil.

@Test
func processError() throws {
  let sut = FailingFeature()
  sut.error = .errorEsperado
  let error = #expect(throws: SomeError.self) {
    try sut.execute()
  }
  if case .errorEsperado = error {
    // Continuar la prueba dado que el error es .errorEsperado
  } else if case .otroError = error {
    // Darle otro manejo a la prueba cuando ocurre otroError
  }
}
Enter fullscreen mode Exit fullscreen mode

Bibliografía

Top comments (0)