Saltar al contenido →

Test asíncronos con XCTest framework

Los test son el pan nuestro de cada día como desarrolladores, una pieza fundamental a la hora de entregar el mejor código posible.

Pero cuando queremos probar una llamada a un servicio REST nos encontramos con el problema de que la llamada se realizaba de manera asíncrona y nuestra función de test pasa “a toda pastilla” sin esperer a ver si ha terminado bien o mal.

XCTestExpectation. Probando… Probando… (async)

La solución a nuestros problemas llegó hace relativamente poco de la mano de la clase XCTestExpectation. Esta clase básicamente actua como un semáforo impidiendo la ejecución del hilo principal hasta que se envía la orden de continuar.

Vamos a ver un ejemplo en el que probamos una operación contra un servicio REST.

func testMovie()
{
    // 214756 - Ted 2
    // Creamos el objeto que nos ayudara a probar 
    // la operacion asincrona
    let expectation: XCTestExpectation = self.expectationWithDescription("Test fanartForMovie()...")

    FanartClient.sharedInstance.fanartForMovie(214756) { (movie, error) -> (Void) in
        // Comprobamos si hay error...
        XCTAssertNil(error, "Se ha producido un error en el framework")

        if let movie = movie
        {
            print("Tenemos datos para la pelicula: \(movie.name)")

            if let posters = movie.posters where !posters.isEmpty
            {
                print("Tiene \(posters.count) posters")
            }

            if let clearart = movie.clearartsHD where !clearart.isEmpty
            {
                print("Tiene \(clearart.count) HD Clearart")
            }

            if let banners = movie.banners where !banners.isEmpty
            {
                print("Tiene \(banners.count) banners")
            }
        }
        else
        {
            print("Esa peli no existe")
        }

        // La operacion asincrona ha terminado
        // Podemos continuar
        expectation.fulfill()
    }

    // Esperamos a que la llamada al servicio termine...
    self.waitForExpectationsWithTimeout(10) { (error: NSError?) -> Void in
        if let error = error
        {
            print("Algo ha ido mal mientras esperamos... \(error.description)")
        }
    }
}

Por cierto, aquellos que caigan en la tentación de usar NSTimeInterval.infinity como parámetro en el método waitForExpectationWithTimeout decirles que se llevarán un estrepitoso error de ejecución.

La secuencia de los hechos ha sido más o menos esta:

  1. Creamos el objeto de la clase XCTestExpectation
  2. Detenemos la ejecución del hilo principal mediante el método waitForExpectationWithTimeout de la clase XCTestCase (la misma de la que hereda nuestra clase de test)
  3. Cuando la operación asíncrona termina invocamos al método fullfill de la clase XCTestExpectation

¿Y si no quiero usar XCTest?

Pues yo te recomiendo que lo uses, pero si a pesar de ello quieres tener tu proyecto de test libre de este framework que sepas que Grand Central Dispatch se convertirá en tu mejor amigo.

Lo que haremos será bloquear el hilo principal hasta que el proceso asíncrono termine, en cuyo momento liberaremos el bloqueo y seguiremos con la ejecución del test.

Para esta tarea nos apoyaremos el la implementación de semáforos disponible en GCD centrada en la estructura dispatch_semaphore_t

A dispatch semaphore is an efficient implementation of a traditional counting semaphore. Dispatch semaphores call down to the kernel only when the calling thread needs to be blocked. If the calling semaphore does not need to block, no kernel call is made.

Las operaciones básicas que haremos con un semáforo son crearlos, esperar a su desbloqueo y desbloquearlo.

// Creacion de un semaforo
let semaphore: dispatch_semaphore_t = dispatch_semaphore_create(0)
// Liberamos el semaforo. 
//La ejecuion puede continuar
dispatch_semaphore_signal(semaphore)
// Este es el punto de bloqueo.
// Aqui esperamos a que se llame
// a la funcion `dispatch_semaphore_signal(sem)`
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)

Y ahora vamos a ver la misma versión del método de prueba anterior pero adaptándolo a GCD.

func testMovie()
{
    // 214756 - Ted 2
    // Este es el semaforo
    let semaphore: dispatch_semaphore_t = dispatch_semaphore_create(0)

    FanartClient.sharedInstance.fanartForMovie(214756) { (movie, error) -> (Void) in
        // Comprobamos si hay error...
        XCTAssertNil(error, "Se ha producido un error en el framework")

        if let movie = movie
        {
            print("Tenemos datos para la pelicula: \(movie.name)")

            if let posters = movie.posters where !posters.isEmpty
            {
                print("Tiene \(posters.count) posters")
            }

            if let clearart = movie.clearartsHD where !clearart.isEmpty
            {
                print("Tiene \(clearart.count) HD Clearart")
            }

            if let banners = movie.banners where !banners.isEmpty
            {
                print("Tiene \(banners.count) banners")
            }

        }
        else
        {
            print("Esa peli no existe")
        }

        // La operacion asincrona ha terminado
        // Podemos continuar
        dispatch_semaphore_signal(semaphore)
    }

    // Esperamos a que la llamada al servicio termine...
    // Y aquí esperamos indefinidamente
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
}

Y la secuencia de hechos es idéntica a la anterior, creamos un semáforo, ponemos el hilo principal en espera y cuando la operación asíncrona termina podemos seguir.

Lecturas recomendadas

Publicado en Swift