Tests de aceptación con Cucumber y Junit 5

0
1139

En este tutorial vamos a realizar tests de aceptación en un entorno spring usando cucumber y junit5.

Índice de contenidos

1. Introducción

Tenemos tutoriales de TDD, BDD y Test de aceptación aquí, BDD con Cucumber aquí, ejemplos de Serenity y Cucumber aquí y un excelente tutorial de BDD y microservicios con Spring Boot aquí. En este nuevo tutorial intentaremos actualizar los anteriores para tener tests de aceptación con Cucumber y Junit 5, no centrándonos en la metodología BDD y dar un enfoque distinto.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil Lenovo t480 (1.80 GHz intel i7-8550U, 32GB DDR4)
  • Sistema Operativo: elementary OS 6.1 Jólnir base: Ubuntu 20.04.3 LTS Kernel: Linux 5.15.0-46-generic x86_64 bits
  • Entorno de desarrollo: IntelliJ IDEA 2022.2 (Ultimate Edition)
  • Apache Maven 3.8.6
  • Java version: 17.0.4, vendor: Eclipse Adoptium

3. Tests de aceptación

El testing es algo que me gusta porque me ayuda a dormir mejor por las noches, si bien los tests no son garantía de que algo funciona, nos dan una sensación (más o menos real) de seguridad. Los tests de aceptación es algo que me parece muy idealista, ya que implican a los desarrolladores y a negocio. Los desarrolladores tienen que estar siempre ahí, porque son los que hacen el software, producto suele estar a más cosas y no suele involucrarse en escribir tests, pero, si tenemos la suerte de que se involucra, nos viene bien conocer este tipo de testing y sacarle el máximo partido.

3.1 Definición

Según la Agile Alliance, una prueba de aceptación es una descripción formal del comportamiento de un software usando un ejemplo o escenario. Para esta descripción formal lo más habitual es usar Gherkin, lo importante es que sea de alto nivel, lo menos técnico posible para poder involucrar a negocio, pero que se puedan ejecutar para que las podamos automatizar e integrarlas con nuestros lenguajes de programación. Si vemos el ejemplo que se propone en la página de Cucumber:

En este ejemplo podemos ver que estamos usando un lenguaje natural muy cercano al inglés donde describimos que es lo que tiene que pasar en el juego de adivina la palabra.

3.2 Beneficios

Al empezar el artículo, me mostraba un poco escéptico con esta técnica, pero sin duda tiene beneficios, ya que fomenta la colaboración entre desarrolladores, usuarios, clientes o expertos del dominio porque los requisitos se deben expresar como un contrato no ambiguo. Cuanto más involucradas estén todas las partes, más probabilidades de éxito tendremos en el proyecto. También podemos dar por hecho que un producto que pasa las pruebas de aceptación será dado por bueno, aunque las partes involucradas pueden refinar estas pruebas o añadir nuevas. Por último al tener estas pruebas de alto nivel se limitan los nuevos defectos y regresiones.

3.3 Errores comunes

A la hora de hacer estas pruebas de aceptación se suelen cometer errores como pueden ser incluir demasiadas definiciones técnicas en los escenarios. Los usuarios y expertos del dominio no tienen por qué entender tecnicismos, lo que hacen estos tecnicismos es que entiendan peor las pruebas de aceptación. Para evitarlo, lo ideal es que los propios usuarios y expertos sean los que escriban las pruebas de aceptación. Como vemos, lo más complicado aquí es conseguir la colaboración entre todas las partes para aprovechar las sinergias.

4. Gherkin

No vamos a extendernos mucho en Gherkin, solo dar unas pinceladas para que todo se entienda mejor. Gherkin es un lenguaje específico del dominio (DSL), que tiene como propósito que sea entendible por negocio y describir el comportamiento del software sin detallar cómo se implementa. Al final, como los propios tests tiene dos propósitos: documentar y automatizar las pruebas. Se puede usar en cualquier idioma (aquí la usaremos en inglés y sería lo recomendable, ya que es el idioma de facto). Los ficheros Gherkin tienen extensión .feature. Cada fichero .feature contiene una única funcionalidad, pero dentro de esa funcionalidad se pueden en tener varias especificaciones. Por ejemplo dentro de la funcionalidad de login podemos tener una especificación para el login correcto, login incorrecto y logout.

La sintaxis esta basada en la indentación de las líneas al igual que otros lenguajes como Python o YAML. Para los comentarios se usa la almohadilla (#)

4.1 Definición de pasos

Los pasos se definen como Given, When, Then que son:

  • Given: Dada una situación donde hacemos una configuración del test.
  • When: Cuando hago algo o un evento llega
  • Then: Entonces espero que suceda algo o una interacción entre colaboradores que puedo comprobar.

Esta forma de organizar los tests es similar a Arrange, Act, Assert.

4.2 Pasos adicionales And y But

Gherkin también cuenta con dos pasos adicionales que son And y But. Estos pasos se usan para mejorar la legibilidad de las especificaciones y añadir más casos al Given y al Then. Un ejemplo donde se todo esto seria:

Tenemos la posibilidad de usar el asterisco (*) para reemplazar cualquier palabra reservada. Esto puede ser útil si tenemos una lista de cosas, reemplazamos la palabra reservada por el asterisco.

Si nos encontramos repitiendo muchas veces el mismo Given, puede significar que no es información necesaria para ese escenario. Para estos casos podemos usar un background. El background nos permite añadir algo de contexto a los escenarios que le siguen. Puede contener uno o más Given, que se ejecutan antes de cada escenario, pero después de Before. Un Background se coloca antes del primer Escenario/Ejemplo, en el mismo nivel de indentación.

4.3 Rule

Desde la versión 6, se soporta la palabra reservada Rule. El propósito de Rule es representar una regla de negocio que debe ser implementada. Proporciona información adicional para una funcionalidad. Rule se utiliza para agrupar varios escenarios que pertenecen a esta regla de negocio. Rule debe contener uno o más escenarios que ilustren la regla en particular.

4.4 Scenario outlines

Cuando los escenarios son parecidos, se pueden usar plantillas y los datos en tablas, de esta forma nos evitamos repetir escenarios.

Esto es solo un repaso de Gherkin, para profundizar se puede consultar la documentación oficial que está en las referencias.

5. Cucumber

Ahora vamos a hablar de la otra pieza que necesitamos: Cucumber. Cucumber es un framework para ejecutar especificaciones Gherkin. Fue escrito originalmente en Ruby, pero actualmente tiene versiones para Java (de la que vamos a hablar) y múltiples lenguajes.

Aunque las especificaciones deberían escribirlas las personas de negocio, para ejecutar una funcionalidad Gherkin (feature) es necesario implementar código Java de pegamento (glue code) que interprete los pasos usando clases del código que es escrito por los desarrolladores.

Ese código Java implementará lo escrito en los ficheros .feature para cargar los valores y los ejemplos de las plantillas. En esos ficheros java tiene que haber correspondencia con los ficheros feature.

Las especificaciones se ejecutan como tests de Junit que pasarán solo si se cumple todo lo que está descrito en la especificación.

6. La aplicación

Para los ejemplos vamos a usar una sencilla aplicación de Spring Boot que tiene un controlador y un DTO para la request de login.

El controlador:

Y el DTO

Para probarlo desde IntelliJ las requests de ejemplo

Vemos que con la primera nos da un 200 y nos devuelve el mensaje Come in

Y la segunda nos devuelve un 403 y el mensaje Not allowed

A partir de aquí tenemos varias opciones

6.1. Usando Spring

Ya que estamos usando spring, una opción es integrar los tests de aceptación con spring. Veamos a alto nivel como seria (nos vale para los siguientes apartados).

Diagrama de alto nivel de cucumber y spring
Diagrama de alto nivel de cucumber y spring

Lo primero que tenemos que hacer es añadir las dependencias que necesitamos que serian: cucumber-java, cucumber-spring y cucumber-junit-platform-engine. Aquí hay que tener cuidado y usar cucumber-junit-platform-engine y no cucumber-junit, ya que cucumber-junit-platform-engine ejecuta los escenarios como tests de Junit 5 y cucumber-junit los ejecuta como tests de Junit 4. También se necesita junit-platform-suite para hacer el punto de entrada de cucumber, ya que se marcó como obsoleta la anotación @Cucumber en favor de @Suite

Después de esto el pom.xml queda así:

Ya que teneos todas las dependencias vamos a crear los tests.

6.1.1 Punto de entrada

La siguiente clase sirve como punto de entrada para cucumber. Esta clase se puede llamar de cualquier forma, pero siempre teniendo en cuenta las convenciones para que sea reconocida como test (normalmente tiene que acabar en test o It dependiendo si queremos que sea unitario o de integración), de lo contrario, maven no encontrará los tests de Cucumber.

Las dos anotaciones @Suite y @IncludeEngines("cucumber") son el equivalente a la antigua @Cucumber, pero que con la versión 7 de cucumber quedó obsoleta. En JUnit 5, podemos definir las opciones de cucumber en el fichero junit-platform.properties, en la carpeta resources de tests. La anotación @SelectClasspathResource("es/dionisiocortes/cucumberjunit/bdd") nos dice dentro de la carpeta resources donde tenemos los ficheros .feature y en @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "es.dionisiocortes.cucumberjunit.bdd") le decimos donde están las clases de pegamento, es decir, los steps.

En el ejemplo anterior, estamos quitando el banner que aparece al ejecutar cucumber y le decimos que no publique los resultados en su servicio. También estamos generando los informes localmente en formato html, json y xml en el directorio target/cucumber-reports.

6.1.2 Configurando spring

Al estar en spring, vamos a configurar cucumber para que pueda hacer uso del contexto de spring. Lo primero seria poner @SpringBootTest.

Vamos a inyectarnos también TestRestTemplate para poder hacer llamadas al api que antes hemos creado.

Ya que tenemos todo configurado, vamos a añadir las pruebas. Un archivo .feature contiene uno o varios escenarios que deben probarse para esa funcionalidad. Cada escenario representa un caso de prueba. Es una parte esencial para Cucumber, ya que es un script que nos automatiza las pruebas, así como documentación.

6.1.3 Ficheros .feature

Vamos a definir nuestro primer fichero .feature:

definición ficheros .feature
definición de los ficheros .feature

Como podemos observar en la imagen, cuando definimos los ficheros .feature, el IDE, en este caso IntelliJ nos dice que nos faltan los pasos para ese fichero, así que vamos a implementarlos.

6.1.4 Definición de los steps

La clase de definición de pasos es el mapeo (o el glue code) entre cada paso del escenario definido en el archivo .feature con un código que implementa esos tests. Cuando Cucumber ejecuta un paso del escenario mencionado en el archivo .feature, escanea el archivo de definición de pasos y averigua qué función será llamada. Esta clase tiene la anotación @CucumberContextConfiguration que hace que Cucumber use esta clase como la configuración del contexto de test para Spring. Si ponemos esta anotación en CucumberSpringConfiguration nos va a dar un error diciendo que hay beans duplicados.

6.1.5 Organización de la aplicación

La estructura de la aplicación queda así:

6.2. Sin usar Spring

En muchas ocasiones queremos que nuestros tests sean lo más agnósticos posibles, no dependan de un framewrok o que estén en otro proyecto o modulo independiente para tener más versatilidad. Aquí vamos a ejemplificar un proyecto multimodulo. El ciclo de vida va a ser, hacer una imagen docker en el proyecto que contiene la aplicación y luego en el otro módulo lanzar los tests.

6.2.1 Haciendo el proyecto multimodulo

Lo primero es el pom.xml padre en el que se han puesto dos módulos (api y acceptance-test), las versiones de todo lo que se usa en el proyecto y todas las dependencias dentro del dependency management. Se queda así:

El proyecto que contiene el API lo hemos cambiado para que solo tenga las dependencias de spring para hacer el API además de actuator para poder ver cuando la aplicación ha terminado de levantar y, como vemos, hemos puesto el plugin de JIB para generar la imagen de docker en la fase de verificación, una vez que ya se han pasado todos los tests y vemos que nuestra aplicación es correcta.

Por último, tenemos el pom del módulo que tiene los test de aceptación. Este módulo solo tiene las dependencias de test. Al no tener spring, usamos rest asured para hacer las llamadas rest, pero no habría problema en usar cualquier librería para hacer las llamadas rest, incluso resttemplate de spring. Tenemos que excluir la dependencia de Groovy porque si no da un error al tener versiones de Groovy distintas, lo cual está descrito en las FAQ de rest-asured. El modulo también tiene el plugin de fabric8 para levantar la imagen que hemos generado anteriormente.

6.2.2 La aplicación

La aplicación no ha cambiado, lo único que se ha hecho es quitar todos los tests de aceptación para que no sean ejecutados. La creación de la imagen docker lo hace el plugin que hemos visto anteriormente en el pom.xml de la aplicación. Lo demás es igual, ya que los endpoints a testear son los mismos. Debemos darnos cuenta que la aplicación ya no tiene dependencias de cucumber porque ya no las necesita y no sabe nada de ese framework.

6.2.3 Los tests

Lo primero de lo que debemos darnos cuenta es que hemos eliminado las dependencias de spring, y la de cucumber relacionada con spring. Se ha añadido rest-assured para hacer las llamadas rest y ya no tenemos la configuración de spring. Si vemos un diagrama quedaría así:

Diagrama de alto nivel de cucumber sin spring
Diagrama de alto nivel de cucumber sin spring

Si vemos la clase cucumberIt es igual a la que teníamos, quedando así:

Se llama CucumberIT para que el plugin de failsafe la ejecute durante la fase de integración, y si revisamos el plugin de fabric8, para cuando llegamos a esa fase, ya tenemos el contenedor levantado, por lo que ya tenemos un sitio al que hacer nuestras peticiones rest.

Los steps tampoco cambian como tal, lo que cambia es su implementación. Vemos que ahora tenemos una configuración de rest-assured para indicarle contra que url base y puerto tiene que ejecutar. Nos guardamos la response en un objeto de tipo Response que luego verificamos con la fluent api que nos proporciona.

6.2.4 La ejecución

Una vez tenemos todo montado, si hacemos mvn clean verify tenemos lo siguiente.

Vemos que nos está diciendo que tenemos 3 proyectos. Luego se pone a compilar y vemos que nos genera la imagen de docker

Ya tenemos la imagen de docker, perfecto. Ahora vamos a levantar esa imagen.

Por último vemos que se ejecutan los tests.

y ya lo tenemos todo.

Por ser maliciosos y confirmar que todo es correcto, si cambiamos el último paso para que espere un 404

Vemos que los tests y la build fallan.

Por último mencionar que dependiendo de la configuración, puede que sea necesario usar el runner de ant si no somos capaces de detectar los tests. Esto está detallado aquí junto con otras configuraciones avanzadas que pueden ser de utilidad.

7. Conclusiones

En este tutorial hemos querido mostrar la importancia de hacer tests de integración y hemos puesto ejemplos con dos escenarios, uno con un framework y otro sin él. Los dos tienen ventajas e inconvenientes. Por ejemplo con un framework podemos acceder a todo el contexto y podríamos tener acceso a más bajo nivel a las cosas. Si tenemos un módulo aparte, es más fácil de portarlo a otro repositorio y que sea más independiente, no dependemos del framework y nos permite ser más puristas, pero puede hacerse algo más engorroso. En definitiva, conviene analizar cada caso, ver lo más conveniente y aplicarlo, cada proyecto es un mundo.

7. Referencias

Dejar respuesta

Please enter your comment!
Please enter your name here