Ya te has acostumbrado a escribir código con async/await, pero ¿alguna vez te has quedado sin saber cómo empezar al preparar las pruebas?
Es especialmente habitual si aún conservas la costumbre de probar el infierno de callbacks añadiendo XCTestExpectation a wait(for:).
Vamos con la conclusión. Con Swift 5.5 o posterior (Xcode 13+), declara la propia función de prueba ** como async, espera el resultado con await y verifícalo con **. El enfoque antiguo con expectations y timeouts ya casi no es necesario.
Hoy explicaré de principio a fin los métodos para probar async/await que he organizado a partir de la práctica profesional.
Resumen de los puntos clave
Para quienes tienen prisa, estos son los puntos esenciales del artículo.
- Añade
async throwsal método de prueba y llama aawaitdentro - Para validar errores, usa
do-catcho el patrón de la versión async deXCTAssertThrowsError - Combina
expectationsolo cuando necesites un timeout - Envuelve las API de callback antiguas con
withCheckedThrowingContinuationpara probarlas
¿Cómo se prueban las funciones async/await de Swift?
Empecemos por la forma más básica.
Antes creábamos una expectation para esperar el resultado asíncrono y llamábamos a fulfill dentro de un closure. El código era largo y difícil de leer.
Ahora es mucho más conciso.
// Solo tienes que añadir async throws a la función de prueba
func test_Cargar el usuario_() async throws {
let service = UserService()
let user = try await service.fetchUser(id: 1)
XCTAssertEqual(user.name, "Lee Seok-woo")
}
En el código anterior solo hay dos aspectos importantes.
Primero, se ha añadido async throws a la declaración de la función. Segundo, tras esperar el resultado con try await, se valida normalmente con XCTAssertEqual.
Al convertir la propia función de prueba en async, el código asíncrono se lee de arriba abajo como el código síncrono.
Desde que cambié a este enfoque, reduje casi a la mitad las líneas del código de prueba.
¿Cómo validar los casos en los que se produce un error?
También probarás con frecuencia situaciones en las que una función asíncrona lanza un error.
El método más intuitivo es do-catch.
func test_Usuario_ inexistente_ error() async {
let service = UserService()
do {
_ = try await service.fetchUser(id: -1)
XCTFail("Lo normal es que se produzca un error")
} catch {
XCTAssertTrue(error is UserError)
}
}
La clave es añadir XCTFail al caso correcto.
Si no se produce ningún error y la prueba simplemente pasa, puede parecer que ha tenido éxito en silencio. Considéralo una protección para evitarlo.
Por cierto, XCTAssertThrowsError del código síncrono no puede recibir directamente una función async de forma predeterminada. Por eso suelo escribirlo explícitamente con do-catch, como arriba.
¿Cómo se prueban las API de callback antiguas?
Lo ideal sería que todo el código hubiera migrado a async, pero la realidad es distinta.
En el proyecto seguramente aún quedan API antiguas que devuelven resultados mediante completion handlers.
En ese caso, envuélvelas con withCheckedThrowingContinuation y llévalas al mundo async.
func fetchLegacy() async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
oldAPI { data, error in
if let error { continuation.resume(throwing: error) }
else { continuation.resume(returning: data!) }
}
}
}
Una vez envueltas, desde las pruebas solo tienes que llamarlas con try await fetchLegacy(), igual que en el enfoque anterior.
Un punto importante: una continuation debe resume exactamente una vez. Si la llamas dos veces, se produce un crash; si no la llamas, la prueba se queda bloqueada para siempre.
Cuándo usar expectations y cuándo pruebas async
He comparado ambos enfoques. (Xcode 13 o posterior, a fecha de 2026)
| Elemento | Enfoque con expectation | Enfoque de prueba async |
|---|---|---|
| Longitud del código | Larga | Corta |
| Legibilidad | Callbacks anidados | Secuencial, de arriba abajo |
| Definición del timeout | Fácil | Requiere gestión adicional |
| Situación recomendada | Esperar notificaciones o temporizadores | La mayoría de operaciones asíncronas |
Para probar funciones async/await habituales, el enfoque async resulta mucho más cómodo.
Sin embargo, cuando un timeout explícito es importante—por ejemplo, para comprobar si una notificación llega dentro de un tiempo concreto o si un temporizador funciona a tiempo—sigue siendo útil combinarlo con expectation.
Resumen
Aunque al principio resulte extraño, basta con añadir async throws una vez a la función de prueba para preguntarte por qué no cambiaste antes.
Si te familiarizas con estos cuatro patrones, podrás cubrir la mayoría de las pruebas asíncronas sin problemas. Aplícalos poco a poco; ¡ánimo!

