Introduction to Microservice End-to-End tests with Cucumber
An introduction to the benefits of using BDD, Cucumber and its Gherkin syntax for End-to-End tests in a microservice architecture, using Java code examples.
Una introducción a los beneficios de usar BDD, Cucumber y su sintaxis Gherkin para pruebas End-to-End en una arquitectura de microservicios, usando ejemplos de código Java.
Complete 4-part guide series
An introduction to the benefits of using BDD, Cucumber and its Gherkin syntax for End-to-End tests in a microservice architecture, using Java code examples.
Create an example Cucumber project in Java with the right package structure, and add an HTTP Client to connect to the backend system.
We use a real project as a practical example of how to implement step definitions with Cucumber in Java, and the best practices to keep state between steps.
Write stable end-to-end Cucumber tests with Awaitility to deal with asynchronous and eventually-consistent systems.
En esta primera parte de la guía, cubrimos los conceptos principales de BDD, TDD, Cucumber y Gherkin. Detallamos las diferencias entre las técnicas y las herramientas, y cómo pueden ayudar a mejorar las formas de trabajo de tu organización.
Cucumber es una herramienta para dar soporte al Desarrollo Guiado por Comportamiento (BDD). Esto significa que no deberíamos saltar directamente a los detalles específicos de esta herramienta sin conocer primero los conceptos centrales de BDD; volveremos a Cucumber más tarde.
El Desarrollo Guiado por Comportamiento (BDD) es una forma de trabajo para equipos de software. Ayuda a las personas a mapear los requisitos de negocio a su implementación de software, usando ejemplos específicos que hacen que los desarrolladores y analistas de negocio entiendan mejor los problemas a resolver.
Uno de los principales objetivos de BDD es hacer que las personas hablen entre sí sobre los casos de uso del sistema. Idealmente, durante estas conversaciones, alguien debería capturar los “scripts de casos de uso” (qué debería suceder cuando un usuario hace esto o aquello), para que puedan usarse como documentación para implementar las funcionalidades correspondientes. Además, estos scripts de casos de uso también pueden automatizarse como pruebas para verificar que el comportamiento del sistema es el esperado.
Por lo tanto, puedes decir que tu equipo está siguiendo los principios de BDD cuando pasas por un proceso iterativo para definir las características de tu sistema usando esas tres etapas: discutir, capturar y escribir pruebas. En la documentación de Cucumber, las encontrarás referenciadas como Discovery, Formulation y Automation.
Dado que uno de los principios de BDD es crear pruebas que puedes automatizar antes de escribir la implementación, BDD y TDD (Desarrollo Guiado por Pruebas) comparten mucho terreno común.
Para mí, BDD es un enfoque mejor y más ambicioso, porque pone mucho énfasis en reunir a las personas para analizar problemas en torno a ejemplos — el lado humano de TI. TDD, por otro lado, pone el énfasis en crear las pruebas antes de escribir el código. Al buscar recursos útiles en línea sobre TDD, es común encontrar artículos que saltan demasiado rápido a los detalles técnicos. El aspecto clave de estas técnicas no debería ser realmente implementar las pruebas antes de escribir la lógica de negocio. La parte más importante es entender el problema antes de comenzar a escribir código, para que resuelvas el problema correcto. No estoy diciendo que TDD no fomente seguir este proceso de descubrimiento, es solo que las definiciones comunes y la documentación sobre TDD se enfocan más en el lado técnico del proceso.
La razón principal de esta diferencia de enfoque es, en mi opinión, que BDD y TDD se aplican normalmente en diferentes niveles de la pirámide de pruebas. Mira la Figura 1 a continuación, o guarda este artículo para después si no estás familiarizado con los conceptos de la pirámide de pruebas. Centrémonos en la primera parte del proceso BDD: personas de negocio, desarrolladores y testers se reúnen en una sesión para refinar los requisitos. Quieres tener estas sesiones a nivel de característica — en Agile: historias de usuario y épicas; pero también a un nivel superior, la visión del software, para capturar los requisitos generales de tu sistema. Sin embargo, durante estas sesiones, normalmente no te adentras en detalles de implementación como qué clases incluirá la solución y la lógica que contendrán. Los scripts de casos de uso que obtienes como resultado de esas sesiones no son suficientes para cubrir todos los casos de prueba que podrías necesitar para Pruebas Unitarias. Encajan mejor en las capas de prueba de Integración y End-to-End (o Aceptación).
Sin embargo, también puedes escribir Pruebas Unitarias antes de escribir la lógica principal de tu programa. Incluso puedes aprovechar herramientas que normalmente se asocian con un flujo de trabajo BDD como Cucumber, BDDMockito o BDDAssertions (AssertJ), y usar una sintaxis Given-When-Then para una mejor legibilidad. Pero, a este nivel bajo, la mayoría de las veces no necesitas involucrar a un analista de negocio o un tester. No necesitas una sesión para definir un método de Prueba Unitaria para verificar si un controlador Java específico devuelve un cierto estado HTTP después de una regla de validación, por ejemplo. Eso está perfectamente bien, porque muchas de estas decisiones son puramente técnicas, detalles de implementación. No sigues todo el proceso BDD a ese nivel porque probablemente no lo necesitas tampoco. Aún así, obtienes los beneficios de TDD cuando escribes la prueba primero porque guiará tu diseño basado en interfaces y casos de uso, incluyendo escenarios de error.
Entonces, desde una perspectiva de forma de trabajo, BDD te ayuda a definir requisitos de alto nivel, mientras que TDD te ayuda a escribir mejores Pruebas Unitarias. Puedes combinar estas técnicas para lograr los mejores resultados. Desde la perspectiva de herramientas, puedes usar herramientas BDD hasta llegar a las Pruebas Unitarias, aunque no estés realmente haciendo BDD a ese nivel.
Algunas personas no logran implementar un enfoque puro de BDD o TDD en sus organizaciones, o simplemente no se acostumbran a escribir las pruebas antes del código.
Esto no es necesariamente problemático. Si eres un desarrollador experimentado, probablemente has trabajado en muchos proyectos donde las pruebas se escriben después de terminar la implementación de la característica. Si aún así discutes adecuadamente los requisitos y los capturas en cualquier formato (como una tarea en un tablero), puede que no sufras de entregar la solución incorrecta.
Aún puedes usar las herramientas BDD, porque mejora la legibilidad de tus pruebas, y puede actuar como un revelador para capturar escenarios infelices que no fueron cubiertos en los requisitos iniciales. Entonces, aún te recomiendo que uses las herramientas BDD incluso cuando estés escribiendo las pruebas al final.
En los primeros capítulos de el libro (recuerda: este es un capítulo extra independiente), pasamos por BDD muy rápidamente porque no quería distraerte del tema principal de esos capítulos. Escribimos Pruebas Unitarias antes de la implementación del servicio siguiendo una sintaxis Given-When-Then. Eso no es BDD.
La razón principal para omitir la parte de Descubrimiento de BDD en el libro es que no es fácil simular cómo ocurriría una discusión entre analistas de negocio, testers y desarrolladores. Introdujimos nuestras historias de usuario como si esas sesiones ya hubieran ocurrido, para mantener un buen ritmo de aprendizaje basado en ejemplos prácticos.
En el futuro, intenta usar BDD para capturar los requisitos de tu sistema, no solo los funcionales sino también los no funcionales. Descubrir deseos como capacidad, rendimiento, tolerancia a fallos, etc., te hará elegir la solución adecuada para tu arquitectura general también. Quién sabe, tal vez descubras que no necesitas un sistema distribuido en absoluto.
Cuando comienzas como desarrollador de software, asumes que algunas partes obvias del trabajo se han hecho correctamente, como capturar requisitos en una Historia de Usuario, por ejemplo.
Parece sencillo: El Product Owner (PO) y/o el Analista de Negocio (BA) hablan con el cliente o analizan datos y las necesidades del mercado, para decidir cuáles son las próximas características a implementar. Luego, el PO y/o BA tienen una reunión con los desarrolladores (y testers, si no son los mismos) para refinar los requisitos, y tener una estimación aproximada del trabajo requerido. Pueden usar esto para calcular el Retorno de Inversión (ROI), que no necesita medirse en moneda real. Por ejemplo, digamos que queremos agregar una nueva insignia a nuestra lógica de gamificación, “Héroe del día”. El requisito inicial dice que deberíamos dar esta insignia a los primeros tres usuarios dentro de 24 horas que resuelvan 3 desafíos de multiplicación. Es una declaración simple, pero tiene algunos desafíos técnicos detrás, como decidir la zona horaria en la que basar esta lógica, o agregar insignias existentes para toda la base de usuarios. No es fácil de implementar. ¿Cuál es el valor de esto para nuestros usuarios? Es agradable, pero probablemente no tan agradable como crear otras 3 insignias que no requieren agregación entre usuarios y lidiar con zonas horarias. Si habilitamos esta discusión en la vida real, probablemente el PO/BA se inclinaría hacia agregar insignias extra más simples.
Aunque ese proceso suena bastante sencillo y es un concepto central en Agile, muchas organizaciones fallan al implementarlo. La mayoría de las veces, es solo un problema de comunicación: las personas no hablan entre sí. Algunas otras veces, hay un problema de presión: las personas piensan que no tienen tiempo para analizar adecuadamente el problema en cuestión, así que es mejor saltar directamente a la implementación. Las sesiones de descubrimiento, refinamientos y reuniones similares simplemente no ocurren, o no se conducen adecuadamente. Los resultados son catastróficos: invertir demasiado tiempo en temas que apenas tienen valor de negocio; crear soluciones que no son lo que los usuarios querían; tener mala calidad de software debido a soluciones temporales y hacks en modo cowboy que no encajan en la arquitectura objetivo, etc.
Solo si has experimentado esos problemas, o si confías en la argumentación anterior, entenderás por qué las personas toman tiempo para documentar y estructurar el proceso para hacer que las personas hablen entre sí. Hay una brecha clara en muchas organizaciones de TI entre las personas de negocio y los desarrolladores.
BDD es una de las herramientas con las que puedes experimentar para llenar esa brecha, habilitando una conversación sobre ejemplos de casos de uso y cómo el sistema debería funcionar en esos escenarios. Para estructurar las sesiones, puedes seguir algunas ideas como se detalla en la página web de Discovery Workshop de Cucumber: Example Mapping, OOPSI o Feature Mapping. También puedes usar tu propio enfoque. Cualquier opción es buena siempre que facilite la conversación sobre los requisitos y evite que las personas salten demasiado pronto a posibles soluciones.
Cucumber es una herramienta muy popular para dar soporte a BDD. Desde una perspectiva de alto nivel, proporciona dos características principales:
Por lo tanto, Cucumber cubre dos etapas de BDD: escribir los casos de uso y automatizar las pruebas.
Gherkin es un lenguaje legible por humanos para definir los casos de prueba de Cucumber. El objetivo principal de su sintaxis es que mantengas las pruebas no técnicas, para que puedan usarse como la documentación de las características de tu sistema.
Al usar BDD, puedes definir los ejemplos de casos de uso usando Gherkin, por lo que es más fácil mapearlos directamente para automatizar pruebas: solo necesitas mapear estas oraciones amigables para humanos a bloques de código.
Usemos un ejemplo. Este es un escenario de prueba real que construiremos en esta guía:
Scenario: Users solve challenges, they get feedback and their stats.
Given a new user John
When he requests a new challenge
And he sends the correct challenge solution
Then his stats include 1 correct attempt
Es bastante legible, ¿no? Este ejemplo de caso de uso (o escenario) representa una de las posibles interacciones del usuario con el sistema que se construye dentro del libro. El usuario obtiene un nuevo desafío de multiplicación, que debe intentar resolver usando cálculo mental. Cuando lo resuelven, obtienen retroalimentación indicando si fue correcto o no, y sus estadísticas se actualizan.
La sintaxis Gherkin es simple y define solo unas pocas palabras clave. Las principales son Given, When y Then, que empleamos para estructurar la definición del caso de uso:
Hay otras palabras clave que puedes usar para evitar la repetición y hacer que tus definiciones de pasos se vean más naturales, como And, But o simplemente *.
Para describir los diferentes casos de prueba, usas las palabras clave Scenario o Example. Lo que escribes después de esta palabra clave no se analizará como una definición de paso, por lo que es realmente de forma libre.
Los documentos Gherkin también deben comenzar con la palabra clave Feature. Después de ella, debes describir cuál es la funcionalidad que estás probando, desde una perspectiva de alto nivel.
Echemos un vistazo a un fragmento del archivo Gherkin que contiene una de las características del sistema (también disponible en Github) para ver estas palabras clave en la práctica:
Feature: Solve multiplication challenges
We present users with challenges they should solve using mental calculation
only. When they're right, we give them score and, in some cases, new badges.
All attempts from all users are stored, so they can see their historical data.
Scenario: Users get new attempts.
Given a new user Mary
When she requests a new challenge
Then she gets a mid-complexity multiplication to solve
Scenario: Users solve challenges, they get feedback and their stats.
Given a new user John
And he requests a new challenge
When he sends the correct challenge solution
Then his stats include 1 correct attempt
Incluso si no leíste el libro, seguramente puedes entender qué hace esta característica. La parte genial es que esto es también nuestro script de prueba. En el código, estos son los contenidos del archivo solving_challenges.feature.
Evita el lenguaje técnico en las características Gherkin
No uses interfaces técnicas en Gherkin, mantenlo legible por un abuelo no desarrollador. Evita definiciones de pasos como Given the database includes record A o When the user calls API X. Eso derrotaría todo el propósito de usar un lenguaje legible por humanos.
Lo más probable es que el escenario mostrado en el Listado 1 no sea exactamente la misma definición de caso de uso que se te ocurriría durante una fase de descubrimiento de requisitos. Un resultado más realista sería algo como Cuando los usuarios envían un intento correcto, (entonces) obtienen retroalimentación indicando que es correcto, y sus estadísticas se actualizan con este nuevo resultado correcto. Más fluido y natural.
El Escenario mostrado arriba ha sido adaptado un poco para hacer que los pasos (cada línea en la definición del escenario) sean reutilizables en diferentes casos de prueba. En Gherkin, puedes lograr esto usando Expresiones de Cucumber para vincular cada paso al método en el código que lo maneja. Esto es porque Gherkin no tiene ningún etiquetado para especificar qué palabras son argumentos (para respetar un lenguaje natural). Definimos eso a nivel de código. Como ejemplo, el último paso se define en código como:
her/his stats include {int} {correct} attempt(s)
La oración anterior es una Expresión de Cucumber, e incluye dos parámetros: {int} y {correct}. El primero es un Tipo de Parámetro incorporado, y hay muchas opciones disponibles (por ejemplo, float, string, etc.). El marcador de posición {correct} es un ejemplo de un tipo de parámetro personalizado, que podemos definir nosotros mismos. Cubriremos esa funcionalidad en más detalle a través de esta guía.
Hay otros usos de sintaxis en la expresión anterior. El carácter / en her/his define texto alternativo, por lo que podemos usar una de las opciones. El (s), como texto dentro del paréntesis, define una parte opcional. Aquí, lo usamos para hacer que el lenguaje se vea un poco más natural.
Una vez que hayas escrito tus características en Gherkin, puedes ejecutarlas como pruebas automatizadas. Como se introdujo antes, Cucumber soporta múltiples lenguajes de programación, Java incluido. La implementación Java de Cucumber se conoce como Cucumber JVM.
Cada paso en las características de Gherkin, con sus expresiones opcionales correspondientes, debe emparejarse con un método en el código Java que implemente la lógica detrás del paso específico. En Cucumber JVM, definimos estos métodos dentro de los archivos de Definición de Pasos. Mira el código a continuación para un extracto de la clase ChallengeStepDefinitions, que mapea el paso que usamos como ejemplo arriba. Entraremos en los detalles en la siguiente parte de esta guía. Por ahora, el concepto clave es que la anotación @Then de Cucumber se usa para proporcionar definiciones de pasos.
public class ChallengeStepDefinitions {
// ...
@Then("her/his stats include {int} {correct} attempt(s)")
public void statsIncludeAttempts(int attemptNumber, boolean correct) {
var stats = this.userActor.retrieveStats();
assertThat(stats).satisfies(s ->
assertThat(s.stream().filter(r -> r.isCorrect() == correct)
.count()).isEqualTo(attemptNumber)
);
}
}
Cucumber JVM se integra con JUnit, por lo que puedes ejecutar las características de Gherkin usando este popular framework de pruebas de Java. Para lograr esto, necesitamos crear una clase que funcione como punto de entrada. Mira el ejemplo a continuación.
package microservices.book;
import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
import org.junit.runner.RunWith;
@RunWith(Cucumber.class)
@CucumberOptions(plugin = {"pretty"})
public class RunCucumberTest {
}
Usamos la anotación @RunWith en JUnit para apuntar el framework a la clase Cucumber, que es una implementación de un JUnit Runner que sabe cómo cargar los archivos .feature y escanea las clases buscando anotaciones de Definición de Pasos (como nuestro ejemplo arriba).
La anotación @CucumberOptions puede usarse para configurar el runner. En el ejemplo anterior, usamos una opción plugin para usar el formateador pretty de Cucumber, que es un plugin incorporado que hace que los reportes generados se vean bien (por ejemplo, otras opciones son json o junit).
En la siguiente Figura, puedes ver cómo estos diferentes elementos se relacionan entre sí en Cucumber JVM.
El siguiente paso es configurar un proyecto esqueleto, algunos buenos ejemplos de casos de uso y comenzar a codificar nuestros pasos. Eso es lo que cubriremos en la Parte 2 de esta guía.
Complete 4-part guide series
An introduction to the benefits of using BDD, Cucumber and its Gherkin syntax for End-to-End tests in a microservice architecture, using Java code examples.
Create an example Cucumber project in Java with the right package structure, and add an HTTP Client to connect to the backend system.
We use a real project as a practical example of how to implement step definitions with Cucumber in Java, and the best practices to keep state between steps.
Write stable end-to-end Cucumber tests with Awaitility to deal with asynchronous and eventually-consistent systems.
Join hundreds of software architects and engineers getting practical insights delivered to their inbox.