Cómo hacer UI testing en iOS con FBSnapshotTestCase
--
Este artículo esta también disponible en mi blog personal
¿Cuántas veces nos pasó de cambiar algún código en un View Controller y que al probar la app en producción la vista haya cambiado mágicamente? ¿No deberíamos tener alguna forma de asegurarnos que la vista no cambie si no es lo deseado?
Es por eso que es importante hacer UI testing. De esta forma, nos aseguramos que, si no queremos cambiar la UI, la app va a mantener la apariencia que siempre tuvo.
En este artículo vamos a centrarnos en el uso de FBSnapshotTestCase para integrar tests de UI a un proyecto muy simple.
Historia de FBSnapshotTestCase
Inicialmente, FBSnapshotTestCase fue creado por el equipo de Facebook. Una herramienta muy útil, que permitía una sencilla implementacion con solo integrar la biblioteca al proyecto e instanciar el View Controller que se quiera testear.
Al parecer, no hubo actualizaciones del SDK en el último año. Según parece, Facebook habría creado otra herramienta más ligada a su uso interno (supongo, algún código más ligado a su propia CI/CD) y es por eso que deprecó el mantenimiento de esta herramienta.
Afortunadamente para nosotros, Uber decidió hacer un fork del proyecto y lo mantiene hoy en día.
Diferencia con Fastlane Snapshot
FBSnapshotTestCase son en realidad tests unitarios que permiten comparar una imagen de referencia de un UIViewController (podría ser también una UIView, pero por lo general se hace por pantalla completa) con la imagen actual que nos daría el proyecto. De esta forma, si hay inconsistencias (diferencias entre la primera y segunda imagen) se genera una tercer imagen con las diferencias marcadas en una escala de grises. Si se genera esta tercer imagen, es decir, si hay diferencias entre la imagen de referencia y la actual del proyecto, se genera un error en ese test unitario y los tests fallan.
Fastlane nos provee una herramienta parecida, Fastlane Snapshot (repositorio) la cual permite realizar capturas de pantalla en los UI tests de Xcode. Cuando terminan todos los tests de UI, si fueron satisfactorios, se genera un HTML con todas las capturas de pantalla. También nos permite por defecto setear los devices y lenguajes que queramos. Esto puede ser bastante útil para probar todas las combinaciones (si tenés 3 lenguajes, 10 pantallas en la app, y soportás 5 dispositivos, te genera 3 x 10 x 5 = ¡150 capturas!). El problema es que es sólo una captura de pantalla, no nos brinda información de si la imagen es la deseada para el producto.
En resumen, FBSnapshotTestCase nos permite comparar y decirnos si tenemos algun error, Fastlane Snapshot solo nos brinda capturas de pantallas en nuestros UI tests de Xcode.
Lista de temas
Vamos a abarcar los siguientes temas, tratando de aprovechar lo más posible el poder que nos da esta herramienta:
- Cómo integrar FBSnapshotTestCase
- Refactorizar código con SnapKit y uso de FBSnapshotCase
- Creación de un script para la generación de pantallas con diferentes resoluciones.
Cómo integrar FBSnapshotTestCase
Para poder empezar el tutorial, vamos a descargar el siguiente projecto de ejemplo en el branch master
https://github.com/fedejordan/FBSnapshotTestCaseExample
Vamos a compilar el proyecto en el simulador (yo usé iPhone 8) y vamos a ver la siguiente pantalla:
Un simple View Controller que podría ser la primer pantalla de cualquier aplicación, con los botones de login y register.
Para poder integrar el SDK de FBSnapshotTestCase yo usé CocoaPods. No debería haber problema integrando el SDK con Cartage o directamente copiando la biblioteca al proyecto. Pueden ver los pasos en la guía del repositorio. De todas formas, los transcribo a continuación:
- En la terminal hacemos
pod init
para crear nuestro podfile - En el podfile ponemos
pod 'iOSSnapshotTestCase'
- Hacemos
pod install
en la terminal
La versión que estoy usando para este proyecto es la 2.1.6
Una vez ya integrado a nuestro proyecto, abrimos el xcworkspace generado por CocoaPods y compilamos el proyecto para ver que todo este bien.
Como dice en la guía oficial de FBSnapshotTestCase, vamos a Edit Scheme -> Run configuration
, para setear correctamente los directorios de las imágenes de referencia y las imágenes de los tests fallidos (las que tienen las diferencias con las imágenes de referencia). Seteamos las correspondientes variables de entorno:
Ahora sí, vamos a crear nuestro primer test unitario con FBSnapshotTestsCase. Para eso, hacemos lo siguiente:
- Creamos un nuevo test de unidad y subclaseamos
FBSnapshotTestCase
en vez deXCTestCase
. - Desde el test, usamos
FBSnapshotVerifyView
para indicar la vista que queramos capturar. Para ello instanciamos la clase ViewController desde el Storyboard. - Correr el test una vez con
self.recordMode = YES;
en el método-setUp
de la clase del test. Esto crea las imágenes de referencia en disco. Se hace cada vez que se quiera hacer un cambio intencionado en una pantalla. - Remover la línea que habilita el modo de grabación y correr el test nuevamente.
Nuestro código debería quedarnos muy parecido al siguiente:
Si vamos al Finder a buscar la carpeta Tests/ReferenceImages
vamos a ver la imagen guardada de nuestro View Controller en un iPhone 8:
Esta imagen es la que se usará para comparar con las siguientes imágenes que genere nuestro proyecto a medida que vayamos agregando o modificando cosas.
Si quieren ver el resultado de implementar el test, pueden hacer un check out
al branch first_snapshot_test
en el repo del cual bajamos el proyecto de ejemplo.
Refactorización del codigo de la vista
Vamos a ver un uso práctico que nos puede dar esta herramienta.
Digamos que empiezo a trabajar en equipo y decido que los Storyboards no son algo escalable, por lo que decido migrar toda la creación de la UI de mi proyecto a código. Investigué un poco, y me gustó una herramienta llamada SnapKit.
No voy a centrarme en detalles de cómo usar esta biblioteca. Así que voy a mostrar directamente el código que resulta de crear la misma pantalla creada anteriormente con Storyboard, esta vez con Snapkit.
Para ello, fue necesario eliminar el Storyboard e instanciar el View Controller desde el AppDelegate. En el test usamos let viewController = ViewController()
para instanciar la clase ViewController
.
Ahora vamos a probar si refactorizamos correctamente la vista con FBSnapshotTestCase, para ellos hacemos Cmd+U.
Finalizado, vemos que obtenemos un error:
Vamos a la carpeta FailureDiffs
y vemos que pasó:
Al parecer, pusimos mal las contraints para la imagen. Como se ve, es el único objeto que parece estar diferente en la UI.
Vamos al código y cambiamos:
make.top.equalTo(titleLabel.snp.bottom).offset(100)
Por esto:
make.top.equalTo(titleLabel.snp.bottom).offset(60)
Probamos con Cmd+U de nuevo, y el test nos debería dar bien.
Pueden bajar el refactor final con SnapKit haciendo un checkout
del branch snapkit_refactor
del git del proyecto de ejemplo.
Generación de las pantallas con diferentes resoluciones
En el ejemplo anterior, vimos como usar la herramienta para un iPhone 8. ¿Pero que pasa si queremos testear en todos los dispositivos?
Tendríamos que configurar los tests para hacerlo en los diferentes simuladores cada vez que queramos. Algo no muy práctico a simple vista.
Es por eso que me tomé el trabajo de crear un script que haga los tests en los dispositivos que se deseen. Algo parecido a la configuración de Fastlane Snapshot, pero sin soporte para varios lenguajes.
Para poder usar el script es necesario que en el metodo setUp()
de la clase de test indiquen self.isDeviceAgnostic = true
asi se agrega el nombre del dispositivo al archivo de imagen.
El script es público y lo pueden ver a continuación. Solamente tienen que cambiar el nombre del workspace
y el scheme
:
Genera como salida los screenshots de todos los dispositivos que se indiquen. Para ver si hubo algun error en alguna captura, basta con ver la salida que genera.
En el ejemplo, solo incluí los dispositivos de tipo iPhone, pero se pueden incluir tambien otros que soporte el proyecto.
Más adelante voy a intentar hacer el mismo script adaptándolo a diferentes lenguajes tambien, con la generación de una web page que muestre todos los resultados. Más parecido a como trabaja Fastlane Snapshot.
Resumen
Aprendimos a como usar una herramienta simple pero poderosa, que nos permite chequear si accidentalmente metimos algún cambio no deseado en nuestra UI.
Vimos un ejemplo de como refactorizar una vista desde un Storyboard a usar SnapKit, y cómo FBSnapshotTestCase nos permite corroborar que hicimos las cosas bien.
Por último, compartí un script que permite la creación de screenshots en tantos dispositivos se le indiquen. Útil cuando tenemos diseños que requieren especial cuidado en todas las pantallas.
Si te gustó el artículo o ves algun error que podrías corregir no dudes en comentarlo o mandarme un mail a fedejordan99@gmail.com
¡Muchas gracias! :D