Pruebas y calidad de código

Cómo probar código asíncrono de Swift con async/await

En Swift 5.5 y versiones posteriores, declara las funciones de prueba como async, espera el resultado con await y luego verifícalo. También se cubren la validación de errores, las pruebas de API de callback antiguas y los casos en los que todavía necesitas usar expectation.

3 min de lectura
Imagen de portada de Cómo probar código asíncrono de Swift con async/await

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.

  1. Añade async throws al método de prueba y llama a await dentro
  2. Para validar errores, usa do-catch o el patrón de la versión async de XCTAssertThrowsError
  3. Combina expectation solo cuando necesites un timeout
  4. Envuelve las API de callback antiguas con withCheckedThrowingContinuation para 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.

Función de prueba async de Swift con la palabra clave await resaltada y una marca verde
Si las funciones async se prueban también como async, ya está

¿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.

Pantalla del navegador de pruebas de Xcode con una marca verde de Test Passed
El momento en que aparece la luz verde es siempre el más satisfactorio

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!