Tipos de tests en Flutter
Todos los que me conocéis sabéis que soy un chapas con Flutter desde hace un tiempo. El caso es que hace unos días una compañera de trabajo que se dedica a automatizar tests, me pregunto si en Flutter se pueden hacer tests, a lo que yo respondí que:
¡¡Por supuesto!!
Para poder testar el código es imprescindible que hagamos código testable. Si no sabes cómo hacer código testable aquí te dejo un pequeño curso de como hacer CLEAN architecture con Flutter. No es un dogma y usa la arquitectura que más te guste pero sí que es una manera de hacer código testable.
https://twitter.com/ficiverson/status/1251622555821187072
Aquí queda una breve explicación para todo el que le interese montar una buena pipeline de tests con Flutter.
Tipos de test cases
En este artículo vamos a cubrir tres tipos de tests que todo developer debería implementar para poder dormir tranquilo cuando hace merge en la rama de PROD :)
Unit tests
Los unit tests evalúan una función, un método o una clase. El objetivo es verificar la corrección de una unidad lógica bajo ciertas condiciones. Todas las dependencias externas deberían ser mockeadas para aislar la pieza de código. Por lo tanto los tests unitarios no necesitan leer de disco, hacer peticiones de red o renderizar una interfaz de usuario.
En Flutter estos tests van en la carpeta test del proyecto y se pueden ejecutar en línea de comandos:
flutter test
Instrumentación
En método setupAll se puede instrumentar el test para preparar las clases que serán sujetas a los diferentes tests de la suite. En este caso, la clase GetNewsUseCase recibe en su constructor una dependencia mockeada para que podamos controlar los valores durante los diferentes tests.
En escenarios reales normalmente usaremos un inyector de dependencias para inyectar la instancia pero para ilustrar bien el concepto decidí dejarlo así.
El método tearDownAll se ejecuta al final de cada test por si tenemos que limpiar algún estado y resetearlo a su valor inicial.
Nombre de los tests
No debemos ser tímidos con los nombres de los tests y cuanto más autoexplicativos mejor. Lo ideal es entender qué parte del código falla solo con ver el report y casi no tener la necesidad de abrir el test.
that can fetch all news from network with a OK result and a list of one new is received on a far far away galaxy :)…
Queda claro que este test prueba que se pueden recuperar las noticias de un data source de red y obtendremos exactamente 1 noticia que viene de una galaxia lejana, muy muy lejana.
Expect
El método expect será en el cual evaluaremos que el resultado de nuestra pieza de código tiene el valor que esperamos
expect(mi valor actual, lo que espero)
Está a disposición del developer una colección muy grande de matchers para comprobar que los valores son los correctos y además podemos encadenarlos para hacer otros matchers combinados. Algunos ejemplos:
- equals(value)
- isNotEmpty()
- isNot(equals(value))
- contains(value)
Mock de los valores
En este caso estoy utilizando Mockito para devolver los valores que me interesan según el test en cuestión. No es obligatorio usar esta librería pero en ese caso la instrumentación de los test será algo más compleja.
when(mockRepository.getNews()).thenAnswer((_) => MockRadiocoRepository.news());
Para que se entienda lo que ocurre aquí es que cuando la pieza de código que estamos evaluando llama al método getNews devolvemos el valor que nos interesa para el propósito de este test.
Widget tests
Los tests de widgets son test que se ejecutan sobre un componente. El objetivo es verificar cómo la UI se ve y cómo interactúa con el usuario. Este tipo de test según la arquitectura que tengáis en el proyecto puede llegar a volverse muy complicado de implementar.
Una de las grandes ventajas de Flutter es que este tipo de test no requieren levantar el sistema de UI y se ejecutan a una velocidad similar a los de unit.
Una gran ventaja para los que estáis aburridos de esperar por Espresso para lanzar la app en Android.
Instrumentación
En este caso estoy utilizando el inyector de dependencias para sobreescribir algunas dependencias con mis mocks. La única diferencia con respecto a los unit que vimos antes es que ahora el estado de las SharedPreferences tienen importancia entre cada ejecución por eso tenemos que limpiar su valor. Por lo demás solo será necesario la siguiente línea para empezar a testar widgets.
WidgetsFlutterBinding.ensureInitialized();
En lugar de usar test(‘loquesea’) ahora tenemos que usar testWidget(‘loqueseaquesea’) y ya estaríamos haciendo test a nuestros widgets.
PumpWidget
El método pumpWidget, que viene en la clase WidgetTester, hace que se haga build y render de ese widget. Una vez lanzado el widget según su instrumentación obtendremos un estado del mismo que podemos evaluar con expect como hacíamos en unit testing.
expect(find.byKey(PageStorageKey<String("news_detail_container"),skipOffstage: true),findsOneWidget);
Este expect de ejemplo busca un widget con una clave concreta y espera que no haya más de uno en el árbol del widget padre.
UI/Driver tests
Este tipo de test lanzan la app real en un dispositivo para verificar que todo su conjunto funciona correctamente. Además también podemos usar este tipo de tests para medir la performance de la app. Los test de este tipo van en la carpeta del proyecto que se llama test_driver. A continuación tenemos que crear un fichero app.dart en la raíz de esa carpeta con el siguiente código:
void main() {
enableFlutterDriverExtension();
app.main();
overrideDependencies();
}
Para lanzar los test tenemos que ejecutar el siguiente comando:
flutter drive --target=test_driver/app.dart
Instrumentación
En la instrumentación podemos decidir si usar la implementación real que haga peticiones de red o si preferimos test de integración independientes de algunos elementos como red, disco, etc.
await FlutterDriver.connect();
Patrón Robot
Para estos test usé este patrón que esconde la implementación de las acciones del usuario en la clase Robot para conseguir que los test de UI sean más legibles dado que enseguida se ensucian con matchers difíciles de leer a golpe de vista. Otra de las ventajas es que el test queda mucho más cerca del lenguaje natural.
¿Podrían a leer estos tests gente no tech?
Yo creo que tiene sentido porque estos test reflejan lo que un usuario puede hacer con la app y sus journeys serán muy próximos a los objetivos de negocio.
Ozzie
Utilizo esta librería para hacer profiling de la app y sacar screenshots del estado final de la IU cuando acaba el test.
El profiling es muy importante porque en este caso se está corriendo la aplicación en un dispositivo real y podemos analizar si en alguna pantalla tenemos algún memory leak.
Es muy interesante mezclar los test de screenshot con los de UI para no duplicar tests. Si tenéis un Design System en vuestro proyecto creo que es el lugar ideal para sacar unas buenas fotos del mismo.
Recapitulando
A continuación veremos las principales características de los diferentes test que hemos evaluado en este artículo.
Unit test
- muy rápidos en ejecución
- estables
- fáciles de implementar
- no evalúan interacciones de usuario
Widget test
- muy rápidos en ejecución
- estables
- complejidad media de implementación
- de alguna manera se pueden ver flows de usuario
UI/Driver test
- muy lentos
- poco estables
- máxima complejidad de implementación
- interacciones reales de usuario
Haciendo los tests me encontré algunas piedras en el camino pero que no son scope de este artículo. Analizando los diferentes ecosistemas de testing para desarrollo nativo y bajo mi experiencia puedo concluir que Flutter nació preparado para ser testable. El mix de nuestro pipeline debe tener en cuenta las características de cada uno para no convertir nuestros tests automáticos en un caos y poder hacer release los viernes con tranquilidad mientras bebemos una 1906.
Modificando un poco el estribillo del gran Kase.O:
Te pierdes lo bueno NO buscando el error
Te pierdes lo mejor
¡¡A hacer test!!
Estaré encantado de resolver cualquier duda, pregunta o sugerencia :)