DEV Community

GoyesDev
GoyesDev

Posted on

[SC] Probando código concurrente con SwiftTesting

Comprensión durante la lectura

¿Qué diferencias fundamentales existen entre XCTest y Swift Testing?

  • No se usa el API de XCTest, o sea clases como XCTestCase o XCTAssert.
  • Swift Testing no soporta el uso de XCTestExpectation.
  • Se prefiere el uso de struct sobre class.
  • Se usan macros como @Test, @Suite, #expect.

¿Por qué las expectations no funcionan en Swift Testing y cómo se reemplazan?

Swift Testing no soporta el uso de XCTestExpectation porque es una biblioteca diferente.

En su lugar se puede combinar el uso de:

  1. withCheckedContinuation(isolation:function:_:) para poder envolver el llamado de un código asíncrono que no tiene async en la firma
  2. withObservationTracking(_:onChange:) para detectar cambios en alguna propiedad y, ante esto, invocar resume() sobre el CheckedContinuation.

¿Qué rol juegan withCheckedContinuation y confirmation() al probar código asíncrono?

withCheckedContinuation sirve para poner un await sobre un código asíncrono que no tenía async en su firma. Este código asíncrono debe llamar resume() sobre el CheckedContinuation.

confirmation(_:expectedCount:isolation:sourceLocation:_:) confirma que cierto evento ocurrió durante el llamado de una función. Este método recibe un body con async en su firma, cuenta cuántas veces se invocó el Confirmation mientras estuvo esperando el body (i.e. await body(confirmation)).

En la práctica este último método sirve para ver si efectivamente hubo la modificación que se esperaba. En el artículo se usó en conjunto con withObservationTracking(_:onChange:) para detectar si hubo un cambio en articleSearcher.searchResults.

¿Cómo se reemplazan setUp y tearDown en Swift Testing?

setUp y tearDown se reemplazan con init y deinit (para este último se requiere usar class en lugar de struct).

¿Por qué no se puede llamar código async dentro de deinit y cómo se soluciona?

Se supone que deinit se llama porque ya se va a liberar el objeto de memoria - No se puede retrasar más esta operación, así que no se puede retener self ni ningún atributo de la prueba.

Para solucionarlo se requiere llamar el código async que se quiere que vaya en el deinit, directamente dentro del método de prueba.

Una solución alternativa consiste en definir un TestScoping, que es un protocolo que le dice al test runner que ejecute un código personalizado antes o después de que ejecuta la suite o función de pruebas. Lo anterior quiere decir que se usa en conjunto con SuiteTrait y/o TestTrait.

Particularmente, si lo que se requiere es manipular el "sut" entonces se lo puede inyectar en una variable estática marcada con el macro TaskLocal para poder usarla desde el contexto de un Task.

Por ejemplo:

struct Environment {
  @TaskLocal static var articleSearcher = ArticleSearcher()
}
Enter fullscreen mode Exit fullscreen mode
struct ArticleSearcherDatabaseTrait: SuiteTrait, TestTrait, TestScoping {
  @MainActor
  func provideScope(
    for test: Test,
    testCase: Test.Case?,
    performing function: () async throws -> Void
  ) async throws {
    print("Running for test \(test.name)")

    let articleSearcher = ArticleSearcher()
    try await ArticleSearcherSwiftTesting.Environment.$articleSearcher
      .withValue(articleSearcher) {
        await articleSearcher.prepareDatabase()
        try await function()
        await articleSearcher.closeDatabase()
      }
  }
}
Enter fullscreen mode Exit fullscreen mode
@Test(ArticleSearcherDatabaseTrait())
func testEmptyQuery() async {
  await Environment.articleSearcher.search("")
  #expect(
    Environment.articleSearcher.searchResults
      == ArticleSearcher.articleTitlesDatabase
  )
}
Enter fullscreen mode Exit fullscreen mode

¿Qué hace exactamente el macro @Test y en qué se diferencia de XCTestCase?

Marca una función como prueba para ser ejecutada por el runner de pruebas.

¿Cuándo conviene usar withCheckedContinuation versus confirmation()?

El primero es para convertir código asíncrono que recibe closures, en async/await. El segundo es para validar si un evento ocurre durante el llamado de una función async.

¿Qué protocolos implementa ArticleSearcherDatabaseTrait y para qué sirve cada uno?

Implementa SuiteTrait y TestTrait. El primero sirve para agregar código antes y después de la ejecución de todos los métodos de la suite de pruebas, y el segundo solo cubre a una sola función de pruebas.

¿Qué hace @TaskLocal y por qué es importante en el contexto de los "Test Scoping Traits"?

@TaskLocal permite acceder a una propiedad dentro de una jerarquía de tareas. O sea que, sin importar el nivel de profundidad dentro de esta jerarquía, siempre que no se haya usado un Task.detached, se puede acceder a ese TaskLocal.


Recordar sin releer

¿Puedes explicar con tus propias palabras cómo funciona el flujo completo de un "Test Scoping Trait"?

¿Cuál es la diferencia práctica entre aplicar un trait a un @Test individual versus a un @Suite?

¿Por qué es obligatorio usar await dentro del closure de confirmation()?


Revisión y síntesis

¿Qué ventajas concretas aporta Swift Testing sobre XCTest para código concurrente?

¿En qué escenarios usarías Test Scoping Traits en lugar de simplemente usar init/deinit?

¿Qué limitaciones tiene aún Swift Testing según el artículo?


Bibliografía

Top comments (0)