Para las siguientes secciones, considerar el siguiente código de producción.
class SUT {
enum Error: Swift.Error {
case x
}
var onSuccess: (() -> ())?
var onError: (() -> ())?
var times = 1
@discardableResult
func execute() async -> Int {
try! await Task.sleep(for: .seconds(10))
(0..<times).forEach { _ in
onSuccess?()
}
return 1
}
func legacyExecute() {
DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in
if let onSuccess = self?.onSuccess {
onSuccess()
} else {
self?.onError?()
}
}
}
}
Probar concurrencia básica
La biblioteca de pruebas se integra con "Swift-concurrency". Para ello hay que marcar la función de prueba con async y, en el cuerpo de la función, hacer await al llamado de algún código asíncrono:
@Test
func test1() async {
let sut = SUT()
let result = await sut.execute() // La prueba espera con await
#expect(result == 1)
}
Confirmar que ocurrió un evento
Al ejecutar código asíncrono que usa "Swift-concurrency", es posible que algún resultado sea devuelto a través de un closure. En este escenario se puede usar confirmation(_:expectedCount:isolation:sourceLocation:_:) para crear un Confirmation. En el closure de este método se escribe la lógica que se quiere probar, que debe de alguna manera tener un async/await. Swift Testing pasa el Confirmation al closure como parámetro, que se invoca como una función dentro del closure que debe invocarse cuando la ejecución es exitosa.
@Test
func test2() async {
let sut = SUT()
await confirmation { confirmation in
sut.onSuccess = { confirmation() }
await sut.execute()
}
}
Notar que no hace falta tener ningún tipo de "timeout" porque el closure de confirmation(_:expectedCount:isolation:sourceLocation:_:) espera con await a que el código invocado termine. Esto trae consigo un problema: ¿Qué pasa si el código invocado es demasiado demorado? Al usar async/await de forma nativa se puede asegurar que el código termina de ejecutar, así que no hay problema.
Consideremos el siguiente ejemplo donde nunca se invoca el Confirmation.
@Test
func test3() async {
let sut = SUT()
await confirmation { confirmation in
// nunca se invoca el confirmation
// sut.onSuccess = { confirmation() }
await sut.execute()
}
}
En caso de que no se invoque el Confirmation se obtiene el siguiente error:
Issue recorded:
Confirmationwas confirmed 0 times, but expected to be confirmed 1 time
Confirmar que ocurra un evento X número de veces
Si se espera que el evento ocurra más de una vez, se debe fijar el valor de expectedCount de acuerdo con el número de ocurrencias esperadas.
@Test
func test4() async {
let sut = SUT()
sut.times = 5
await confirmation(expectedCount: 5) { confirmation in
sut.onSuccess = { confirmation() }
await sut.execute()
}
}
Si el número de ocurrencias es diferente del esperado tendremos un error muy parecido al que señalé antes:
Issue recorded: Confirmation was confirmed 4 times, but expected to be confirmed 5 times
Confirmar que ocurra un evento nunca ocurra
Para validar que un evento nunca ocurra, hay que crear un Confirmation con expectedCount de 0
@Test
func test5() async {
let sut = SUT()
sut.times = 0
await confirmation(expectedCount: 0) { confirmation in
sut.onSuccess = { confirmation() }
await sut.execute()
}
}
Confirmar que el número de ocurrencias de un evento está dentro de un rango
En el caso de que el número exacto de veces que ocurra un evento cambie en el tiempo o sea aleatorio, se puede usar un rango:
-
1...: El evento debe ocurrir al menos una vez. -
5...: El evento debe ocurrir al menos cinco veces. -
1...5: El evento debe ocurrir al menos una vez, y máximo cinco. -
0..<100: El evento puede o no ocurrir, pero nunca debe ocurrir más de 99 veces.
@Test
func test6() async {
let sut = SUT()
sut.times = Int.random(in: 1...5)
await confirmation(expectedCount: 1...5) { confirmation in
sut.onSuccess = { confirmation() }
await sut.execute()
}
}
Probar código legado sin Swift-concurrency (async/await)
Al probar código asíncrono que no usa async/await dentro del closure de confirmation(_:expectedCount:isolation:sourceLocation:_:), como no hay ningún await, confirmation termina inmediatamente y, como nunca se invocó confirmation() entonces la prueba aparece como fallida.
@Test
func test6() async {
let sut = SUT()
await confirmation { confirmation in
sut.onSuccess = { confirmation() }
sut.legacyExecute()
}
}
En la documentación dice:
Confirmations function similarly to the expectations API of XCTest, however, they don't block or suspend the caller while waiting for a condition to be fulfilled. Instead, the requirement is expected to be confirmed (the equivalent of fulfilling an expectation) before confirmation() returns
Lo anterior significa que Confirmation asume que si no lo han confirmado cuando llega al fin del bloque es porque la prueba falla. Confirmation no espera a que se cumpla la expectativa.
Para solucionar esto, hay que envolver el código legado que usa closures con withCheckedThrowingContinuation de forma que se invoque continuation.resume() cuando la ejecución haya sido exitosa y continuation.resume(throwing:) en caso de que haya ocurrido algún error.
@Test
func test7() async throws {
let sut = SUT()
try await withCheckedThrowingContinuation { continuation in
sut.onSuccess = {
continuation.resume()
}
sut.onError = {
continuation.resume(throwing: SUT.Error.x)
}
sut.legacyExecute()
}
}
El problema de la implementación anterior es que no se puede asegurar que el código legado termine, así que puede hacer falta un timeout. Más sobre esto en el próximo artículo.
Top comments (0)