Un recorrido completo por la técnica que facilita la colaboración entre el área de producto y el área técnica
POR Ariel Zeballes and Alan Rosas, Software Engineers EN etermax
En este artículo, continuando nuestra serie relacionada al lenguaje Flutter, desarrollaremos un ejemplo muy simple utilizando los conceptos de Desarrollo guiado por el comportamiento. Como el nombre de la técnica indica, todo se centra en el COMPORTAMIENTO que queremos lograr en nuestra aplicación. Básicamente, sería el “QUE” queremos obtener.
Tomaremos la aplicación por defecto que nos crea Flutter y haremos algunos pequeños cambios (permitiéndonos algo de sobrediseño) para poder mostrar las principales características de la técnica.
Para no extendernos demasiado, haremos sólo un ciclo de iteración (solo un escenario), pero lo suficientemente simple y completo para poder hacer un recorrido completo por la técnica.
El código completo del ejemplo se puede obtener desde el siguiente repositorio, donde en la rama main tenemos la solución final y las ramas step-# contienen el código del paso a paso utilizado en cada iteración. Adicionalmente, en cada una de las secciones siguientes disponemos los links a cada uno de los archivos de código fuente en su respectiva rama.
El siguiente mapa de palabras menciona las herramientas que vamos a utilizar…
Nuestro primer escenario
Sin más preámbulos vamos a crear la característica “Contador” con el escenario inicial de nuestra aplicación (primera iteración) features/counter.feature
Línea por línea este escenario indica que el contador tiene un valor 10, mostraremos ese valor luego de reiniciar la aplicación.
?debemos reiniciar la aplicación luego de asignar el valor 10 al contador porque como veremos la aplicación se inicia antes de ejecutar el escenario.
Ahora que tenemos especificada la primera iteración (y la única en nuestro ejemplo ?), generaremos el test de integración asociado que nos guiará en la implementación.
Creación de la aplicación
Comenzamos creando una aplicación en flutter para un dispositivo android
flutter create --template=app --platforms android --project-name \
flutter_bdd --org com.example .
Como framework BDD vamos a utilizar el paquete flutter_gherkin.
Vamos a agregarlo al proyecto y crear la carpeta test_driver:
#Agregamos el paquete flutter_gherkin
flutter pub add flutter_gherkin --dev
#Creamos la carpeta test_driver
mkdir test_driver
#Eliminamos el test de widget ejemplo
rm test/widget_test.dart
Creamos una aplicación para pruebas de aceptación que habilita el driver de automatización y a su vez ejecutará la aplicación a desarrollar.
Podemos ejecutar la aplicación de prueba en un emulador de dispositivo Android mediante:
#Obtenemos la lista de emuladores disponibles
emulator -list-avds
#Ejecutamos un emulador. Por ejemplo Pixel_4_API_30
emulator @Pixel_4_API_30 &
#Ejecutamos la aplicación de prueba en el emulador
flutter run test_driver/app.dart
Como siguiente paso vamos a crear el punto de entrada a la configuración de nuestros tests de aceptación mediante el archivo:
Y ahora podemos ejecutar nuestras pruebas de aceptación en el emulador
#ejecutar el emulador
emulator -avd Pixel_4_API_30 &
#ejecutamos el test de aceptación
dart test_driver/app_test.dart
El test debería fallar debido a que no hemos implementado los pasos asociados al feature
...
Step definition not found for text:
'Given counter value is {10}'
...
Podemos observar en la salida de la ejecución que nos sugiera la forma en que debemos crear y configurar los pasos asociados.
? Implementación de pasos del test
Given counter value is {10}
A continuación, implementaremos el primero de los pasos de nuestro escenario que se debería encargar de asignar un valor inicial al servicio contador.
Para ellos agregamos el archivo test_driver/steps/given_counter_value_is.dart
Agregamos el paso a la lista de definiciones de pasos en test_driver/app_test.dart
Si ejecutamos nuevamente nuestro test de aceptación, veremos que ahora se nos solicita la implementación del siguiente paso:
Step definition not found for text:
'Then I see the value 10'
Then I see the value 10
De manera similar agregamos el paso ‘Then I see the value 10’ en test_driver/steps/then_i_see_the_value.dart
Y nuevamente lo agregamos a la lista de pasos personalizados en test_driver/app_test.dart
Ahora cuando ejecutamos el test vemos que el mismo falla por el motivo correcto ya que la aplicación no está mostrando el valor inicial deseado.
...
Step 'Then I see the value 10' did not pass, all remaining steps will be skipped #
...
Vale la pena mencionar que en ningún momento hemos asignado el valor inicial a nuestro servicio contador. Esto vamos a dejarlo pendiente debido a que para efectuarlo, antes necesitamos conocer algunos detalles acerca de cómo podemos lograrlo.
Mejoras en el código
Para simplificar la implementación separamos los tres componentes principales de la aplicación cada uno en su archivo
Y también vamos a eliminar la lógica por defecto asociada a la vista
Implementación
El ciclo BDD básicamente consiste en identificar un escenario de uso de la aplicación. Automatizar la prueba de ese escenario para que falle. Luego realizamos las modificaciones necesarias a la aplicación para hacer pasar el test haciendo uso de TDD (reiterados ciclos red, green, blue).
El ciclo termina cuando la prueba de aceptación pasa, momento en el cual podemos abordar nuestro siguiente escenario.
Habiendo identificado que necesitamos obtener un valor de contador inicial desde un servicio externo a la aplicación. Vamos a adentrarnos en el ciclo interno de TDD y abordaremos la construcción de cada uno de los componentes de la aplicación.
La comunicación entre vistas y modelo la vamos a efectuar siguiendo el patrón MVVM.
La vista
Comenzando con el ciclo interno de TDD vamos a crear las pruebas asociadas a la vista de nuestra aplicación.
Agregamos los siguientes paquetes como dependencias de desarrollo para poder probar las interacciones de nuestro vista con su modelo vista.
flutter pub add mockito --dev
flutter pub add build_runner --dev
Utilizamos el paquete build_runner para generar los “mocks” de prueba. Para efectuar lo anterior podemos dejar en ejecución el siguiente proceso, el cual detectará los archivos que incluyan la etiqueta @GenerateNiceMocks generando el código necesario.
flutter pub run build_runner watch --delete-conflicting-outputs &
Agregamos el test de vista test/my_home_page_test.dart
Modificamos la vista para que utilice su viewmodel lib/my_home_page.dart
Agregamos el viewmodel lib/my_home_page_view_model.dart
Por último creamos el viewmodel desde el componente principal lib/my_app.dart
Para ejecutar las pruebas de nuestra vista podemos hacer
flutter test test/my_home_page_test.dart
ViewModel
Para las pruebas de nuestra modelo vista necesitaremos simular interacciones asíncronas por lo que agregaremos el paquete fake_async. Esto nos permitirá probar las interacciones del componente con el servicio contador.
flutter pub add fake_async --dev
No debemos olvidarnos de que la etiqueta @GenerateNiceMocks necesita de la ejecución de build_runner que hemos visto anteriormente.
Agregamos las pruebas que especifican el comportamiento deseado de nuestro viewmodel y su interacción con el servicio contador:
test/my_home_page_view_model_test.dart
En resumen nuestro modelo vista inicialmente mostrará el valor 0 para el contador. La vista podrá solicitar la obtención del valor actual del contador mediante el método initialize. Cuando se obtiene el valor se debe actualizar la interfaz de usuario.
La interfaz del servicio CounterService es lib/counter_service.dart
La implementación de nuestro modelo vista queda de la siguiente manera:
lib/my_home_page_view_model.dart
#Validamos nuestro modelo vista mediante la ejecución de sus pruebas
flutter test test/my_home_page_view_model_test.dart
Podremos notar que tenemos errores de compilación debido a que aún no tenemos una implementación concreta del servicio.
Servicio contador
Con respecto a nuestro servicio contador vamos a suponer que el mismo es externo y accesible a través de una API REST.
Entonces vamos a implementar nuestra versión HTTP del servicio:
Agregamos un servicio de configuración que nos permita obtener la dirección base de nuestro servicio desde una variable de entorno.
lib/configuration_service.dart
#Agregamos el paquete http
flutter pub add http
Creamos la prueba con la especificación del servicio
test/http_counter_service_test.dart
E implementamos el mismo
Ahora que tenemos una implementación concreta del servicio contador, podemos corregir el error de compilación en my_app.dart:
#Ejecutamos las pruebas de nuestro servicio
flutter test test/http_counter_service_test.dart
De vuelta a ‘Given counter value is {10}’
Ahora que conocemos los detalles técnicos del servicio contador vamos a asignar un valor inicial desde el paso de test.
Para ello vamos a simular el servicio mediante una instancia de wiremock
Levantamos un contenedor Docker con una instancia del servicio
docker run --rm -d -p 8080:8080 --name wiremock wiremock/wiremock
Y agregamos el mapeo de respuesta en el paso correspondiente:
lib/test_driver/steps/given_counter_value_is.dart
Debemos configurar la variable de entorno en la aplicación que vamos a probar:
Ahora podemos ejecutar nuestra prueba de aceptación de la siguiente forma:
#Use current local ip address
dart --define=COUNTER_BASE_URL\=http://192.168.100.29:8080 test_driver/app_test.dart
y podremos observar que la prueba se ejecuta correctamente con lo cual podemos dar por finalizada nuestra primera iteración en el ciclo BDD ?.
Comentarios finales
Como hemos visto en las secciones anteriores, y reforzando lo dicho al comienzo del artículo, la técnica BDD se focaliza en el comportamiento objetivo que quiero obtener de la aplicación (el QUÉ), dejando de lado los detalles técnicos (el CÓMO) para los ciclos internos de desarrollo. En general, facilita las charlas entre ingeniería y producto utilizando un lenguaje común (escenarios). Por último, es un excelente indicador de estado de avance del desarrollo ya que rápidamente se obtiene software funcionando, lo cual permite a su vez, bajar el nivel de incertidumbre inicial y descubrir o modificar escenarios no contemplados.