¿Cómo hacer un desarrollo guiado por el comportamiento en Flutter?

Flutter Bdd

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.

test_driver/app.dart

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:

test_driver/app_test.dar

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

lib/my_home_page.dart

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.

BDD/TDD Cycle

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

lib/http_counter_service.dart

Ahora que tenemos una implementación concreta del servicio contador, podemos corregir el error de compilación en my_app.dart:

lib/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:

lib/test_driver/app_test.dart

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.

, ,