A falta de pan, buenas son galletas. No?
Voy contar un poco cómo hago test en iOS, cubriendo con los test todos los casos de uso. Además esto vale para todos, incluso para los que pensáis que no tenéis tiempo de hacer test.

Creo que no sería la primera vez que cualquiera escuchamos, ojalá yo tuviera tiempo para hacer test… Que envidia, en tu empresa te dejan tiempo para probar…

Una y otra y otra vez escuchamos esto en todos sitios. Pero, ¿es cierto eso?. NO. Creo que hay modos de hacer testing en nuestras apps sin necesidad de invertir “más tiempo”, de hecho, no solo no vamos a invertir tiempo, sino que vamos a ahorrar con respecto al desarrollo normal.

La verdad que con lo de moda que esta el testing y los artículos que vemos por ahí, donde nos enseñan hasta como probar nuestros test, es lógico que el primer pensamiento que tengamos ante los test es que hay que invertir mucho tiempo, pero veamos…

Invirtiendo tiempo.

Es probable que al principio tengamos que invertir un pelín más de tiempo, sobre todo si no conocemos como funcionan XCTest, o alguna de las librerías que recomiendo (Que la verdad, son muy sencillas). Después de hacer un test que cubra un caso de uso, veréis como la mayoría son prácticamente iguales, nos saldrán de forma mecánica.

Como hacer Quick Tests 🐉

Si creéis que vamos a usar QuickERROR Quick nos quita alguna función útil de XCode, como la de “correr?” un sólo test. Ademas, aunque mole mucho hacer un describe, context, it, el nivel de indentación que cogen nuestros ficheros de test muchas veces puede ser cuanto menos incómodo.

Si hubiese escrito esto hace sólo un par de semanas, os habría hablado maravillas de Quick, que su principal función es que hagamos BDD(Behavihor Driven Development) de un modo sencillo y fácil. Pero si hacer BDD con este framework nos crea mas problemas que ventajas, mejor dejémoslo de lado.

Además, hace unos días, ejecutando uno de estos test, un caso de uso llegaba a comportarse de forma muy extraña… No se cómo manejaba Quick los bloques que va recibiendo, pero en cierto momento, usando Alamofire por debajo para la validación del statusCode por ejemplo, aún recibiendo un código 404, sólo cuando se llamaba en un sitio muy exacto con Quick, Alamofire.request.validate() se lo saltaba y directamente entraba en el response.result.success al tener el result en un switch.

Para empezar.

¿Cómo hago los test?, Vamoh a olvidarno de los test unitarios. Sí, los test unitarios son buenos, probamos de forma isolada que una “unidad” de nuestro código funciona bién, o que peta (Porque los tests están ahí para cascar, que es lo que nos avisa de que metemos la gamba, no para ver que tenemos un 130% de cobertura y twittear lo mucho que mola y que los jefes vean todo verde en nuestro Jenkins o Travis o lo que queráis, cuando dentro del test puedes llamar a toda la app y hacer assert(true, true, "Por que yo molo...") de esta forma conseguimos un 130% de cobertura de esa que no vale pa Na (con chiste 100tífico y todo 😂😂))

Bien, en mi caso particular hago test unitarios bajo necesidad. ¿Cuándo? pues de esas veces que escribes algo que no tienes del todo claro que vaya a funcionar, o escribimos un pequeño objeto para hacer no se qué historia y queremos saber que funciona bien… En ese momento y solo en ese momento, hago un nuevo fichero para probar esa funcion, compruebo que funciona bien cuando quiero y si tiene que lanzar algún tipo de Error, pues también lo pruebo. Porque es casi más importante probar que nuestro código falla cuando tiene que fallar antes de que funcione bien… Si algo no funciona bien es muy probable que se refleje sin mucho esfuerzo en nuestra aplicación, pero si ante XThing esperamos XError y no ocurre, eso no se va a ver en ningún jodido sitio, cuando se vea se presentará como bug del DENOMIO y no queremos eso. Así que, si en algo invertimos tiempo sobre todo es en: Vamos a probar cuando nuestro programa falla

¿Que test hago yo? Pues los únicos test que no entienden de razas, test de integración (😂 chistaco rico pa tu body eh?). Con los test de integración probamos flujos enteros, además, si tenemos un entorno de pruebas en nuestra empresa, podemos matar dos pájaros de un tiro. Podríamos según un parámetro a la hora de ejecutar nuestros test, estos salgan a internet o no.

¿Como ahorrar tiempo?

No se vosotros, pero cada vez que compilo la maldita aplicación para hacer una prueba y tengo que llegar a la pantalla donde vaya a probar lo que sea que estoy probando, es un infierno. No por nada en particular, pero compila, llega a la pantalla X (no digo nada si para llegar a esa pantalla tienes que cumplir X condiciones antes, como seleccionar cosas o rellenar más campos), rellena los campos de esa pantalla… llega el momento de probar el nuevo caso de uso y PUM, falla, pon puntos de depuración, mira por donde esta fallando, habiendo dejado logs será mas fácil, pero sigue siendo un coñasso… Arregla lo que crees que está fallando, volvemos a hacer todo lo anterior, llega a la pantalla de nuevo…. Y así sucesivamente hasta que probamos que funciona, que muchas veces sólo probamos que funciona, porque para que salte el error raro que queremos probar las condiciones que tenemos que tener en el simulador son algo raras y no podemos crearlas a veces desde ahí, por lo que no comprobamos que falla siempre que tiene que fallar…

Con esto, no vamos a probar animaciones, si cuando una tabla recarga los datos, no lo hace con una animación como tu tenías pensado, te vas a dar cuenta probando la app, o se va a dar cuenta alguien que pruebe la app. Pero siempre he preferido que me digan, oye mira, aquí falta esta animación, o que esta fuente sea mas grande… me da igual, pero mejor eso a: mira, ayer estuve haciendo esto, y en este caso, cuando llegas aquí peta. Y eso en el maravilloso caso de que la otra persona sepa reproducir el error, que si no, estas bien jodido.

Empezando a probar nuestros casos de uso

Cómo hacemos esto para ahorrar tiempo… Probamos lo que realmente estamos haciendo. ¿Que quiere decir? vamos a hacer tests que sean lo menos frágil posible. Sinceramente, nunca he encontrado el sentido a comprobar que si llamamos a X función, tengamos que verificar que esa función ha llamado a su vez a 4 cosas por dentro 3 veces, que ha tocado no se qué y tal. Porque si la otra función ha tocado algo de un ámbito global, o tiene un side effect, lo mas probable es que esa función no esté bien diseñada, hagamos funciones pequeñas y especializadas. (esto no lo digo yo, lo dice Robert C Martin)

Para hacer test que no sean frágiles, la mejor solución (personalmente) es comprobar el output de la función y SÓLO el output. Si llamamos a la función suma, comprobemos que la función suma ante el input 2 y 2 devuelve 4, y que ante el input 9 y 9 devuelve 18 y probemos también que pasa si los parámetros que recibe son Int y le pasamos un Int.max, de este modo sabremos que nuestro programa no peta porque se salga del rango que admite un Int o cosas así.

Probemos que se comporta como debe.
Comprobemos el comportamiento (BDD), de este modo, haciendo test de caja negra donde no sabemos que pasa dentro, nuestros test serán mas fuertes. Porque si hoy la función suma suma dos Int con un + (vamos a viajar a java por breves momentos) y el día de mañana esa función crea un BigDecimal (el tipo recomendado para hacer operaciones con números debido a cómo maneja los decimales, sobre todo cuando empieza a haber dinero de por medio (creo recordar 🤔)), nuestro test seguirá funcionando, y seguirá funcionando bien.

Entonces, siguiendo esa idea, la mayoría de nuestros casos de uso se conectan a la red, o a nuestra base de datos, o a ambas… Sí, al principio vas a necesitar una mínima inversión de tiempo para “mockear” esto, pero hecho una vez, reusamos…

La red:

Para que la red funcione como quiero y devuelva lo que quiero, uso OHHTTPStubs, es una pasada. Podemos simular de todo, podemos devolver lo que queramos, que nuestra red falle, que la conexión sea lenta, que no haya conexión… Hay otra opción que es Nocilla, pero tiene demasiada azúcar (50%) y no funciona con Swift 3 (😂)

Pero no penséis que vamos a llenar nuestros test de strings que contienen JSON y cosas así. Seguramente, antes de hacer una llamada a un nuevo endpoint de vuestra API, seguro que probáis que funciona y para ver que devuelve a través de algún cliente REST (Hace poco una de esas noches de no dormir, descubrí Insomnia. Otro chistaco pa tu body, pero la app es genial!). Bien, pues el json (o el xml, o lo que sea) que devuelve lo podemos copiar y pegar en un ficherito .json, que posteriormente usaremos desde nuestros test.

Además hacer el setup con OHHTTPStubs es pinta y colorea, por lo que no tendremos muchos problemas, con echarle un ojo a la documentación (que tiene una pagina entera dedicada a ejemplos de uso) no nos costará que nuestra red devuelva lo que queramos. Y sí, sin hacer un sólo mock. Por lo que no vamos a gastar tiempo en hacer un double de test de la capa de nuestro programa que llame a la red. Ademas, también nos provee de un método de clase .cleanAllStubs() que llamaremos en nuestro tearDown o afterEach para quitar todos los stubs que hayamos puesto.

La base de datos:

Yo personalmente cuando tengo que usar una base de datos, uso CoreData. Entonces para probar la base de datos, lo único que cambio es que el stack de CoreData sea en memoria, ademas de tener por ahí un par de helpers para limpiar toda la BDD, o tener 4 objetos preparados para hacer las pruebas.

Cómo en la mayoría de casos, en realidad, de donde nos vengan los datos nos debería dar igual para probar nuestro caso de uso, no es algo necesario, pero si queremos hilar mas fino, con alguna justificación, si que podemos hacer alguna que otra verificación de que algunos datos de nuestra BDD han sido actualizados, creados o se han borrado.

Por ejemplo: en la aplicación esta de reproducir música de youtube que estoy haciendo, cuando tenga la parte de guardar las canciones y tal, si que voy a probar todo lo que tiene que ver con BDD, es decir, los casos de uso de actualizar una lista de reproducción, para que así vea que las canciones se han borrado, se han creado las nuevas… Porque en esa aplicación, que estén o no, es de vital importancia (Una aplicación que se basa en reproducir música offline no tiene sentido si nuestra BDD funciona mal). Pero ese es un caso, a lo mejor en vuestra aplicación no tiene mucho menos sentido. Eso, como otras muchas cosas, a gusto del consumidor.

Los test

En esta parte, hablo de como me organizo en iOS, pero si trabajáis con Android, seguro que esto se puede portar a Android o a cualquier otra plataforma sin mucho esfuerzo. Sólo remarcar que un target aquí, por así decirlo, define que clases son las que tendrán en cuenta en esa compilación, por lo tanto, siendo estas clases Tests, define que test van a correr.

Ahora, organicemos nuestros test. Como estos test son de integración, es decir, hacen cosas muy distintas a los test unitarios que corremos cada 5 minutos en nuestro día a día (otro chistaco 😂😜), yo suelo crear un target a parte para ellos. Un target nuevo con su propio Scheme, que lo único que hace es correr todos nuestros test de integación. Así, los tenemos todos agrupaditos.

Una vez tenemos el target, yo personalmente no lo he hecho, y sinceramente, se me ha ocurrido según escribía esto, pero estoy seguro de que podemos poner un flag en el Scheme que corre este target y así decidir si hacemos el setup de stubs con OHHTTPStubs o no. De este modo, si queremos, podemos hacer que nuestros test sean realmente test de integración y salgan a la red. Pero perdemos la ventaja de controlar nosotros esa calidad de red, aunque en realidad pasen a ser bastante mas reales. Pero claro, tampoco llegan a ser reales del todo, porque la conexión de nuestro portátil (o nuestro CI) sea mucho mejor que la de cualquier persona real que use nuestra aplicación en el metro.

Cosillas a tener en cuenta

Cuando nuestros casos de uso tengan como side effect cambiar el estado de un Singleton, debemos tenerlo en cuenta. Si necesitamos el uso de tokens para la autenticación del usuario y tenemos por ahí algo que llama al keychain, ese keychain es el mismo siempre, por lo que deberíamos limpiar lo que se guarda por ahí para que nuestros test sean completamente idependientes.

No creas que la inversión de tiempo inicial es tanta, crear el target y configurar su Scheme es cuestión de minutos. Empezar a tirar nuestros test según escribimos nuestro código (que no hacer TDD), tampoco es lo mas complicado del mundo. Además tendremos la ventaja de que comprobamos bastantes capas de nuestra aplicación. Por cierto, esta idea de probar flujos, no solo se puede hacer desde el caso de uso. Me explico: Yo suelo tener organizado algo para inyectar dependencias y que las cosas se creen de forma sencilla, así obtengo cada Interactor (últimamente los llamo UseCase directamente, por legibilidad) con una linea. Según lo tengo hecho para producción, como no necesitamos mocks, lo tengo Test Ready. Pues en lugar de coger los casos de uso, podemos tirar directamente desde el presenter, haciendo un mock de la vista y que esta vaya “tocando” cosas y luego esperar para que la vista reciba X inputs, pero como eso lleva mas tiempo del que tengo, tiro desde el caso de uso y hago pruebas de lo más importante.

Tenemos el control sobre la red, sobre su calidad, velocidad y directamente con si esta apagada o no. También controlamos todos los parámetros de entrada de nuestro caso de uso, en caso de que este tenga que hacer alguna validación por dentro, lo cubrimos con los test y no tenemos que estar escribiendo en el simulador. También podemos cubrir los casos en que al json del API le falte un campo y ver que pasa con nuestro programa. Los test pueden darnos mucha información sobre el funcionamiento de nuestro código, y no sólo a nivel de que funcione bien, sino como he dicho antes, cuales son sus puntos débiles y donde puede fallar.

Otro consejo, cuando encontremos un bug en nuestro programa, porque uno de nuestros casos de uso este fallando mientras los test están pasando bien, es de vital importancia ir a donde probamos ese caso de uso y añadir un test que haga saltar ese bug. De este modo, empezaremos también a tener cubierto los bugs encontrados y sabremos que están arreglados, no sólo lo sabremos sino que tendremos pruebas. Así que si eso mismo vuelve a pasar, la culpa es de los de back 😁😂. El método este de test por bug, no solo es útil en este tipo de test, sino en todos aquellos test que tengamos. Y si no tenemos, es un buen momento para empezar, escribir un test unitario es cuestión de minutos también, por lo que podemos encontrar donde esta fallando nuestro programa, escribir un test que saque el fallo a relucir antes de arreglarlo. En estos casos es muy importante asegurarnos de que el test falla (porque si lo arreglamos/creemos que lo hemos arreglado, ver un test verde es lo normal, pero no nos asegura NADA dado que no le hemos visto fallar) porque cuando pase, será prueba inequívoca de que el test prueba lo que tiene que probar y lo prueba Bien.

Un ejemplito.

Personalmente, lo que uso en mi día a día con los test es lo siguiente. Esto del target que os he dicho, OHHTTPStubs para devolver lo que quiera/necesite en la red y Nimble, porque sus asserts sí que me parecen mas explicativos que los de XCTest, pero como he dicho antes, a gusto del consumidor. Mucho cuidado con Nimble en cosas mas allá que los expect().to(), para el tema de cosas asíncronas, siempre usaría XCode, escribir un expectation no es nada complicado y funciona mucho mejor que Nimble (experiencia personal). El otro día, estuve 4 horas con un falso negativo, es decir, un test que fallaba cuando en realidad funcionaba bien, esto debido a que tenía un expect de una notificación, Nimble usa un objeto por debajo que se suscribe al NotificationCenter y recoge todas las notificaciones que suelta. ¿Donde puede haber un fallo ahí?, pues como además en ese momento estaba usando Quick, debía de haber algún problema de concurrencia o algo así, porque cuando lanzaba la notificación su bicho de recoger notificaciones no detectaba ninguna notificación lanzada, y eso que la linea de lanzar notificación se ejecutaba perfectamente y todo… Pues usando el expectation de XCode, la pilló a la primera y sin problemas, por estas cosas a veces es malo usar librerías de terceros no?.

Si ademas tenéis vuestra aplicación en OBJ-C pero te mueres de ganas de picar Swift, en los test es un buen sitio donde empezar, así tienes un campo de pruebas entero a tu disposición donde puedes empezar a ver que tal se entiende entre sí y todos sus entresijos.

En el caso este, probamos que cuando mandamos un email con un usuario a un endpoint que es /user/forgot. Vamos a probar ambos casos, cuando el email se encuentra en base de datos y cuando el email no se encuentra en base de datos. Cuando el email se encuentra en base de datos es el típico 204, que no tiene que devolver nada y cuando no lo encuentra nos devolverá un json que se “mapeará” a un objeto HTTPError.

Aquí os dejo todo el código para que le echéis un vistazo:

final class ForgotPasswordFlowSpec: XCTestCase {

  let sut: ForgotPasswordUseCase = UseCaseBuilder().resolve()

  override func tearDown() {
      super.tearDown()
      OHHTTPStubs.removeAllStubs()
  }

  func testFlowSuccess() {
      self.setUpSuccessStubs()

      let request = ForgotPasswordRequest(email: "julian@diariodeprogramacion.com")

      let forgot = expectation(description: "Forgot passsword use case")
      self.sut.execute(request).then { response in
          expect(response).toNot(beNil())
          forgot.fulfill()
      }.catch { error in
          fail("Not expected error: \(error)")
          forgot.fulfill()
      }    
      self.waitForExpectations(timeout: 1.0, handler: nil)
  }

  func testFlowMailNotExists() {
      self.setupFailSutbs()
      let request = ForgotPasswordRequest(email: "mail@not.exists")

      let forgot = expectation(description: "Failing forgot passsword use case")

      self.sut.execute(request).then { response in
          fail("Not expected success with response :\(response)")
          forgot.fulfill()
      }.catch { error in
          guard let error = error as? HTTPError else {
              fail("Error must be HTTPError type")
              forgot.fulfill()
              return
          }
          expect(error.statusCode).to(equal(404))
          forgot.fulfill()
      }      
      self.waitForExpectations(timeout: 1.0, handler: nil)
  }

  private func setUpSuccessStubs() {
      stub(condition: isHost(HOST) && isPath("/user/forgot") && isMethodPOST()) { request -> OHHTTPStubsResponse in
          return OHHTTPStubsResponse(data: Data(), statusCode: 204, headers: nil)
      }
  }

  private func setupFailSutbs() {
      stub(condition: isHost(HOST) && isPath("/user/forgot") && isMethodPOST()) { request -> OHHTTPStubsResponse in
          return fixture(filePath: path("ForgotPasswordMailNotFoundError"), status: 404, headers: nil)
      }
  }

}

fileprivate final class UseCaseBuilder: UserUseCaseBuilder { }


Ahora, a probarlo. No dudes en preguntar algo si no ha quedado claro por meail o twitter, o simplemente decir porqué no estás de acuerdo.