DEV Community

GoyesDev
GoyesDev

Posted on

[SC] Swift Concurrency Extras (Point-Free)

Comprensión durante la lectura

¿Por qué las pruebas asíncronas en Swift pueden ser inestables (flaky) cuando se usan tareas no estructuradas?

Aunque el artículo era sobre pruebas inestables en relación con asincronismo, en realidad tenía que ver con la incapacidad de detectar un cambio interno en un método sobre una variable que podía cambiar varias veces y una prueba de caracterización no servía.

¿Qué es withMainSerialExecutor y cuál es su propósito dentro de los tests?

La biblioteca "Swift Concurrency Extras" ofrece el método withMainSerialExecutor(operation:) que ejecuta cierto código en el executor serial del MainActor. Esto implica que, para que funcione, tiene que aislarse la prueba o la suite de pruebas en MainActor.

Por otro lado, es obligatorio que donde se use withMainSerialExecutor se defina la suite como serial:

@Suite(.serialized)
@MainActor
final class ImageFetcherSwiftTesting {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Se usa de la siguiente manera:

await withMainSerialExecutor {
  // Everything performed in this scope is performed serially...
}
Enter fullscreen mode Exit fullscreen mode

En el artículo se usa en conjunto con await Task.yield() para hacer que una Task ceda la ejecución a otra. Particularmente, se tenía una Task en la prueba que envolvía el llamado al código de producción que también ejecutaba una Task (i.e. downloadImage: (URL) async throws -> Data).

¿Cómo funciona Task.yield() y por qué su comportamiento varía dependiendo del ejecutor?

Task.yield() cede el funcionamiento a otra tarea que esté esperando. Si la tarea en cuestión tiene la prioridad más alta, entonces vuelve a tomar el control.

Si el executor está ocupado con alguna otra prueba, entonces el comportamiento de Task.yield() no va a conmutar entre dos Task como se pretende. Por esta razón, se aísla cierto bloque de código con withMainSerialExecutor.

¿Cuál es la diferencia entre usar Task.yield() con y sin el ejecutor serial principal?

Sin el ejecutor serial principal, no se puede garantizar conmutación entre dos tareas.

¿Cómo se integra esta solución con XCTest y con Swift Testing respectivamente?

@Suite(.serialized)
@MainActor
final class ImageFetcherSwiftTesting {
  /// By running the test using a main serial executor, we allow ourselves to use `Task.yield()` and read state in between.
  @Test
  func isLoadingWithMainSerialExecutor() async throws {
    try await withMainSerialExecutor {
      let imageFetcher = ImageFetcher(downloadImage: { url in

        /// Yield here to allow the test to continue evaluation. This allows us to check the `isLoading` state.
        await Task.yield()

        /// Return plain `Data()` for this test as we're just interested in the `isLoading` state.
        return Data()
      })

      /// Call into the `fetchImage` method to ensure the `isLoading` state flips to `true` and `false`.
      let task = Task {
        _ = try await imageFetcher.fetchImage(
          imageURL: URL(string: "https://example-image.url")!
        )
      }

      /// Suspends the current test task and allow the above task to start executing up until `downloadImage(...)` is called.
      await Task.yield()

      /// Check the `isLoading` state while we're 'paused' just at `downloadImage`.
      #expect(imageFetcher.isLoading == true)

      /// Await the result of the image fetching.
      try await task.value

      /// Validate that `isLoading` restores to `false`.
      #expect(imageFetcher.isLoading == false)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

¿Por qué el artículo recomienda usar .serialized en suites de Swift Testing?

withMainSerialExecutor sobreescribe el ejecutor global de Swift con el ejecutor serial principal. Por esta razón, cuando se usa, todas las tareas quedarán encoladas en el ejecutor serial principal hasta que el scope del ejecutor termine.

Si se ejecutan las pruebas en paralelo, esto afectará a las pruebas simultáneamente. Por esto se debe serializar la suite de pruebas.


Recordar sin releer

¿Podrías explicar con tus propias palabras cómo el ejecutor serial resuelve la inestabilidad de los tests?

¿Qué advertencias hace el autor sobre el uso de withMainSerialExecutor?


Revisión y síntesis

¿En qué escenarios de tu propio proyecto aplicarías esta técnica y en cuáles no sería necesaria?

¿Qué limitaciones tiene este enfoque y cómo las mitigarías?


Bibliografía

Top comments (0)