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 {
// ...
}
Se usa de la siguiente manera:
await withMainSerialExecutor {
// Everything performed in this scope is performed serially...
}
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)
}
}
}
¿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.
Top comments (0)