Swift Testing #6: Aislando bugs conocidos usando withKnownIssue

withKnownIssue sirve para marcar una sección de una prueba como un error conocido que no debería hacer fallar la prueba, pero cuya resolución se quiere detectar en el futuro. Si el objetivo fuera simplemente ignorar una prueba fallida durante el desarrollo, es mejor deshabilitarla con .disabled.

@Test
func expectedIssue() {
  let sut = FailingFeature()
  sut.executeWithUnderlyingError()
  #expect(sut.expectedIssue == false) // ❌ Falla
}

En el ejemplo anterior, se sabe que el código señalado tiene un bug así que la prueba falla. Si no se puede arreglar el problema subyacente, se puede envolver el código que falla con withKnownIssue.

@Test
func expectedIssue() {
  let sut = FailingFeature()
  sut.executeWithUnderlyingError()
  withKnownIssue("BUG #123") {
    #expect(sut.expectedIssue == false) // Reporta: Known issue
  }
}

De esta forma, la prueba no se marca como fallida, sino que se reporta que hay un error conocido vigente. Además, se puede incluir un comentario para explicar el problema o dar contexto.

¿Cuándo usarlo?

Bug conocido

Mi prueba falla debido a un bug ya reconocido. Dejar que se ejecute la prueba puede implicar que el CI falle. No se quiere borrar la prueba, puesto que aporta documentación e información que no debe ser olvidada.

Documentación de deuda técnica

A veces el comportamiento incorrecto es tolerado temporalmente. Quizás en el momento no se cuente con los recursos para solucionar el problema, o tal vez haya algún problema de más prioridad. withKnownIssue permite dejar constancia explícita dentro de la prueba.

Detectar cuando un problema se “arregle solo”

Si en una futura versión de la dependencia (o del código propio) el comportamiento empieza a ser correcto, el bloque dejará de fallar y el framework reportará que el error conocido ya no ocurre, provocando que la prueba falle. Esto avisa que se puede quitar el marcador.

import Dependency
struct DependencyWrapperTests {
  @Test
  func basic() {
    let sut = DependencyWrapper(dependency: SomeDependency())
    whenKnownIssue("Bug #1234: cálculo incorrecto de fechas") {
      let result = sut.compute()
      #expect(result == "2025-11-25")
    }
  }
}

En el ejemplo anterior se sabe que la dependencia está fallando y se obtiene un comportamiento incorrecto. Se conoce la falla, pero todavía no se puede resolver.

¿Cuándo no usarlo?

  • No se debe usar para silenciar fallos durante el desarrollo.
  • No se debe usar para comportamientos indefinidos o temporales de la prueba.

Envolver código arrojado como conocido

Supongamos que cierto bloque de código puede arrojar una excepción y, aunque se espera que no lo haga, bajo ciertas condiciones está ocurriendo. Esto puede considerarse como un bug y también puede envolverse con withKnownIssue. En este caso no basta solo con envolver la línea que arroje la excepción, sino que también se deben mover las líneas que cuentan con la correcta ejecución del código.

@Test
func withKnownIssueWrappingThrowingCall() {
    let sut = FailingFeature()
    withKnownIssue {
        try sut.executeWithError()
    }
}

Diferencia entre withKnownIssue y #expect(throws:)

expect(throws:) sirve para esperar que el código pasado por closure arroje un error específico. Sabiendo esto cabe preguntarse cuándo usar withKnownIssue en su lugar.

La diferencia entre estos dos operadores radica en su intención: expect(throws:) quiere decir “espero que esto lance una excepción” pero posiblemente hace parte de un comportamiento normal, correcto y esperado. En cambio, withKnownIssue comunica que hay un fallo incorrecto, pero que está reconocido y se rastrea.

Capturar una falla específica

Por defecto, withKnownIssue considera todas las fallas que ocurran al invocar el closure que tiene como cuerpo. En caso de que pueda haber varias fallas, se las puede discriminar con el closure matching:, de tipo KnownIssueMatcher, que se invoca una vez por cada falla (Issue) recibida para determinar si debe ser tratada como conocida.

En el siguiente código sut.executeWithNoError() puede potencialmente arrojar un error, aunque se diseñó para que no lo haga en este ejemplo. Por otro lado, #expect(1 + 1 == 3) está mal y provocaría que falle la prueba. En el closure definido en matching: se revisa la falla recibida y retornar true para indicar que es conocida (o sea que no falla la prueba), o false para indicar que la falla (Issue) debe provocar la falla (“failure”) de la prueba.

Específicamente en matching: se está definiendo que todo error capturado, emitido por algún throw, es una falla conocida. Al mismo tiempo, todo #expect fallido también es una falla conocida. Cualquier otra razón de fallo no se considera como conocido y provoca el fallo de la prueba.

@Test
func withKnownIssueWithMatchingClosure() throws {
  let sut = FailingFeature()
  try withKnownIssue {
    try sut.executeWithNoError() // No arroja error
    #expect(1 + 1 == 3) // Este expect falla
  } matching: { issue in
    if case .errorCaught(let error) = issue.kind {
      // Puede capturar la excepción de sut.executeWithNoError()
      // Al retornar true indico que es un error conocido, 
      // así que la prueba no continúa y su estado queda en "Known issue"
      return true
    } else if case .expectationFailed(let expectation) = issue.kind {
      // Captura la falla de #expect
      // Si retorna true, indico que es una falla conocida, 
      // así que la prueba para y su estado queda en "Known issue"
      return true
    } else {
      // En cualquier otro escenario, la prueba falla
      return false
    }
  }
}

Resolver una falla conocida

Si withKnownIssue ya no captura la falla esperada, entonces provocará que la prueba falle debido a la ausencia de fallas esperadas en el código. Esto notifica que el problema subyacente ya fue resuelto y que se debe revisar la prueba para eliminar el whenKnownIssue en caso de que ya no sea necesario.

Capturar una falla dadas ciertas precondiciones

En ocasiones se sabe que una falla conocida aparece ante ciertas condiciones. En este caso se puede usar el closure when: (que recibe Void y retorna Bool) para indicarlas. Si las condiciones no se dan (i.e. el closure when retorna false), la prueba falla. En caso contrario, la prueba aparece con “falla conocida” en el reporte.

En el siguiente ejemplo, la prueba no falla si precondition == 7, incluso aunque se espere tener una precondición par.

@Test
func preconditions() {
  let sut = FailingFeature()
  sut.precondition = 7
  withKnownIssue {
    #expect(sut.hasEvenPrecondition())
  } when: {
    sut.precondition == 7
  }
}

Manejar una falla no-determinística

Si el problema subyacente es impredecible y falla de forma aleatoria (como, por ejemplo, debido a una condición de carrera), se puede pasar isIntermittent: true para especificar que la falla no siempre ocurre. De esta forma la prueba no fallará si la falla conocida no se presenta.
En el siguiente ejemplo, cuando sut.precondition es par, la prueba pasa. En caso contrario, la prueba se queda en estado “Known issue”.

@Test
func intermittentPrecondition() {
  let sut = FailingFeature()
  sut.precondition = Int.random(in: 0..<10)
  withKnownIssue(isIntermittent: true) {
    #expect(sut.hasEvenPrecondition())
  }
}

Bibliografía

  • Documentación sobre Swift Testing, aquí.
  • Documentación “Known issues”, aquí.
  • Artículo “Introduction to Swift Testing”, aquí.
Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post

Transformando WordPress en una Herramienta Sostenible: Plugins y Configuraciones Eco-Amigables

Next Post
beyond-vanity-metrics:-how-to-prove-product-marketing’s-business-impact

Beyond vanity metrics: How to prove product marketing’s business impact

Related Posts