¡Compártelo!

Cucumber: automatizar y estructurar pruebas en un entorno BDD

Cucumber es un framework que da soporte para aplicar la metodología de desarrollo BDD (Behavior Driven Development). Esta metodología se basa en poner el foco desde el inicio en cómo el usuario interactúa con la aplicación. Es complementaria y una evolución de TDD.  Utiliza el lenguaje Gherkin para especificar las pruebas.

De los lenguajes de programación que soporta Cucumber vamos a utilizar Java, aunque también existe soporte para Node, Ruby, Android, Kotlin… 

Cómo automatizar test con Cucumber

Entorno

  • Cucumber 7.9.0
  • Java 17
  • Maven 3.8.6
  • JUnit 5
  • Selenium 4.6.0 
  • Eclipse + Plugin Cucumber + Plugin Natural (Soporte a sintaxis Gherkin)
  • Sitio web sobre el que automatizar las pruebas: GreenKart.

Los ejemplos de este post están accesibles en el repositorio de github.

Estructura del proyecto

Se parte de un proyecto maven y se añaden las siguientes dependencias:

Los ficheros con las features deben estar en el classpath de los test:

Cucumber

Las clases java deben tener el mismo paquete raíz. A partir de él se estructuran en base su responsabilidad:

Cucumber

En el paquete pageobjects tenemos aquellas clases que utilizan el api de Selenium. En stepdefinitions las clases que mapean los steps de las features de Cucumber. 

Para poder ejecutar escenarios de mediante JUnit es necesario configurar una clase Runner, en nuestro caso RunCucumberTest:

@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("features")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.example.cucumber.automation")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "html:target/cucumber.html,json:target/cucumber.json,junit:target/cucumber.xml,"
+ "com.aventstack.extentreports.cucumber.adapter.ExtentCucumberAdapter:")
public class RunCucumberTest {
}

Además de anotar con @Suite se precisan las configuraciones específicas de Cucumber:

  • @IncludeEngines: Indicamos que son test basados en Cucumber.
  • @SelectClassPathResource: Los ficheros de features están en /features dentro del classpath.
  • @ConfigurationParameter(GLUA_PROPERTY_NAME): El paquete común para todas las clases de test.
  • @ConfigurationParameter(PLUGIN_PROPERTY_NAME): Plugins que vamos a utilizar para generar informes de ejecución.

También se podría utilizar TestNG en lugar de JUnit, adaptando la configuración.

Features

Su propósito es describir a alto nivel los comportamientos del software y agrupar escenarios relacionados. Se escriben en lenguaje Gherkin que sigue una estructura muy cercana al lenguaje natural.

En este ejemplo trabajamos con una web de comercio electrónico. Queremos especificar el comportamiento del carrito de la compra. Para ello indicamos dos escenarios.

  • El carrito inicialmente está vacío
  • Al añadir productos al carrito se proporciona la funcionalidad de crear pedido con el contenido de la cesta de la compra.
# -- FILE: features/Cart.feature
Feature: User experience with Cart

  Scenario: The cart is initially empty
    Given User is on GreenCart landing page
    When user inspect the cart
    Then verify the cart is empty

  Scenario Outline: Add product to cart
    Given User is on GreenCart landing page
    When user searched with Shortname <Name> and extracted actual name of product
      And added 4 items of the selected product to cart
    Then verify user has ability to checkout order

  Examples:
  | Name |
  | Cucum  |
  | Carrot  |

Analicemos la estructura del fichero:

  • Feature: Tiene el propósito de agrupar escenarios y hacer una descripción a alto nivel de los mismos. 
  • Scenario: Es un ejemplo concreto que concreta una regla de negocio. Además representa un test que se ejecuta mediante Cucumber. Se subdivide en varios Steps secuenciales:
    • Given: Contexto inicial/precondiciones que se cumplen en el inicio del test
    • When: Describe un evento.
    • Then: Salida esperada tras la ejecución del evento.
  • Dentro de cada step se pueden utilizar las keywords And/But para unir sucesivas condiciones.
  • Scenario outline: Se utiliza para ejecutar el mismo escenario N veces con distintos valores. Se combina con la keyword Examples.
  • Examples: Datos en forma tabular que se inyectan en el escenario. La primera fila son los nombres de las variables y las siguientes filas los valores de las mismas. En el ejemplo la primera ejecución reemplaza <Name> con el valor “Cucum” y la segunda con “Carrot”. 
  • Argumentos en Steps
    • Doc Strings: Permite pasar parámetros al step definition. En el ejemplo tenemos un step que se parametriza con el valor 4. Permite reutilizar el step en distintos escenarios.
  • Data Tables: Permite pasar una lista de valores al step definition.
Scenario: Home Page Default Login
    Given User is on landing page
    When User sign up with following credentials
    | nati | jkwx | nati@domain.com | Spain | 33888 |
    Then Home page is populated

Es buena práctica que la descripción en lenguaje natural sea concisa. Debe evitar referencias a la implementación. Es preferible “el usuario añade el producto al carrito y accede a su contenido” frente a “el usuario pulsa en el botón de añadir y a continuación en el icono del carrito”. El comportamiento de implementación lo daremos en los StepDefinition a nivel de clases Java.

Step definitions

Es un método en java con una expresión que está vinculado con uno o varios Steps de Gherkin. Cuando Cucumber procesa un determinado fichero de features busca por la definición correspondiente del step definition y lo ejecuta.

Una vez creados los ficheros feature si ejecutamos los test unitarios el propio framework nos proporciona en la salida una propuesta de los métodos a implementar.

mvn test
io.cucumber.junit.platform.engine.UndefinedStepException: The step 'User is on GreenCart landing page' and 2 other step(s) are undefined.
You can implement these steps using the snippet(s) below:

@Given("User is on GreenCart landing page")
public void user_is_on_green_cart_landing_page() {
    // Write code here that turns the phrase above into concrete actions
    throw new io.cucumber.java.PendingException();
}
@When("user inspect the cart")
public void user_inspect_the_cart() {
    // Write code here that turns the phrase above into concrete actions
    throw new io.cucumber.java.PendingException();
}
@Then("verify the cart is empty")
public void verify_the_cart_is_empty() {
    // Write code here that turns the phrase above into concrete actions
    throw new io.cucumber.java.PendingException();
}

Crearemos la siguiente clase:

public class CheckoutPageStepDefinitions {

@Given("User is on GreenCart landing page")
public void user_is_on_green_cart_landing_page() {
          Assertions.assertTrue(landingPage.getTitle().contains("GreenKart"));
}
@When("user inspect the cart")
public void user_inspect_the_cart() {
          checkoutPage.inspectTheCart();
}

@Then("verify the cart is empty")
public void verify_the_cart_is_empty() {
          Assertions.assertEquals("Your cart is empty!", checkoutPage.getCartContent());
}

}

Las anotaciones de la librería @Given, @When, @Then junto con el texto de las mismas son las utilizadas por el framework para hacer el matcheo con la especificación de los ficheros feature.

Dentro de cada método se implementa la lógica correspondiente al step. Delegamos en las clases LandingPage y CheckoutPage en las que encapsulamos la lógica de selectores e interacciones mediante el api de Selenium. También utilizamos las aserciones de JUnit 5 para realizar aquellas verificaciones precisas.

public class LandingPage {

private WebDriver driver;

public LandingPage(WebDriver driver) {
super();
this.driver = driver;
}

public String getTitle() {
return driver.getTitle();
}

}
public class CheckoutPage {

private static final By CART_BAG = By.cssSelector("[alt='Cart']");
private static final By EMPTY_CART = By.cssSelector(".empty-cart > h2");

private WebDriver driver;

public CheckoutPage(WebDriver driver) {
super();
this.driver = driver;
}

public void inspectCart() {
driver.findElement(CART_BAG).click();
}

public String getCartContent() {
WebElement cartContent =                   new WebDriverWait(driver, Duration.ofSeconds(2))         .until(ExpectedConditions.visibilityOfElementLocated(EMPTY_CART));
return cartContent.getText();
}
}

Step definitions con parámetros

Se pueden utilizar expresiones regulares o expresiones de Cucumber para capturar partes del step en Gherkin y pasarlo como parámetros a los step definitions. Veamos un ejemplo:

@And("added {int} items of the selected product to cart")
public void added_items_of_the_selected_product_to_cart(Integer quantity) {
landingPage.incrementQuantity(quantity);
landingPage.addToCart();
}

El método será el seleccionado para ejecutar cualquier step en los ficheros feature que haga match con la expresión indicada. Además se encarga de hacer la conversión de tipos entre el String del fichero feature y el tipo deseado, en este caso Integer.

Step definitions con data tables

Para mapear datos en los ficheros de feature con forma tabular el framework proporciona la clase DataTable. Dispone de una api completa para utilizar de distintas formas de utilizar los datos.

@When("User sign up with following credentials")
public void user_sign_up_with_following_credentials(DataTable dataTable) {
System.out.println(dataTable.asLists().get(0));
}
[nati, jkwx, nati@domain.com, Spain, 33888]

Hooks

Son bloques de código que pueden ejecutarse en distintos puntos del ciclo de vida de Cucumber. Un uso típico que se les da es para configurar pasos previos y posteriores a cada escenario:

@After
public void afterScenario() {
testContextSetup.getTestBase().webDriverManager().quit();
}

@AfterStep
public void addScreenShot(Scenario scenario) {
if (scenario.isFailed()) {
WebDriver webDriver = testContextSetup.getTestBase().webDriverManager();
              byte[] screenShot = webDriver.getScreenshotAs(OutputType.BYTES);
scenario.attach(screenShot, "image/png", "image");

}
}

La anotación @After hace que el método se ejecute después de cada escenario. En este caso se emplea para cerrar el navegador.

La anotación @AfterStep es para que el método se ejecute a continuación de cada step. En este caso la utilizamos para comprobar si ha fallado el escenario y tomar una captura de pantalla. Será de ayuda para localizar el problema en escenarios con errores. Lo veremos en la sección de reporting.

Tags

Permiten organizar features y escenarios de Cucumber. Se pueden utilizar con distintos propósitos:

Se definen en los ficheros feature:

@AcademicTest
  Scenario: Default Login
    Given User is on landing page
    When User login into application with username "joselll" and password "456321"
    Then Home page is populated

Podemos emplear dicho tag para no ejecutar en un momento dado todos los escenarios que vayan anotados con él, en el starter de JUnit

@ConfigurationParameter(key = FILTER_TAGS_PROPERTY_NAME, value = "not @AcademicTest")
public class RunCucumberTest {
}

O con mayor prioridad mediante la instrucción en línea de comandos:

mvn test -Dcucumber.filter.tags="not @AcademicTest"

Inyección de dependencias

Una buena decisión de diseño es tener varias clases separadas con distintos métodos que mapean los StepDefinitions. En dichas clases se precisa acceder a estado común, valores obtenidos en una página que deben coincidir en otra, WebDriver, los distintos PageObject, utilidades comunes, …  

Cucumber ofrece varias alternativas. En este caso vamos a ver como utilizar Cucumber Picocontainer como implementación del patrón IOC.

public class OfferPageStepDefinitions {

private TestContextSetup testContextSetup;
private String offerPageProductName;

public OfferPageStepDefinitions(TestContextSetup testContextSetup) {
this.testContextSetup = testContextSetup;
}

@Then("^user searched for (.+) shortname in offers page$")
public void user_searched_for_shortname_in_offers_page(String shortName){

testContextSetup.getPageObjectManager().getLandingPage().selectTopDealsPage();
testContextSetup.getGenericUtils().switchWindowToChild();

OfferPage offerPage = testContextSetup.getPageObjectManager().getOfferPage();
offerPage.searchItem(shortName);
offerPageProductName = offerPage.getProductName();

}

@Then("validate product name in offers page matches with landing page")
public void validate_product_name_in_offers_page_matches_with_landing_page() {
Assertions.assertEquals(              offerPageProductName, testContextSetup.getLandingPageProductName());
}
}

La inyección de la dependencia con TestContextSetup se realiza a través del constructor de la clase OfferPageStepDefinitions. La utilizamos para obtener la referencia LandingPage y OfferPage. Empleamos sus métodos para implementar la lógica del step. Además compartimos estado entre steps definidos en otras clases, almacenando el valor de un nombre de producto en la página principal y buscando dicho nombre en la página de ofertas.

public class TestContextSetup {

private String landingPageProductName;
private PageObjectManager pageObjectManager;
private TestBase testBase;
private GenericUtils genericUtils;

public TestContextSetup() {
super();
this.testBase = new TestBase();
this.pageObjectManager =                   new  PageObjectManager(testBase.webDriverManager());
this.genericUtils = new GenericUtils(testBase.webDriverManager());
}
public String getLandingPageProductName() {
return landingPageProductName;
}
public void setLandingPageProductName(String landingPageProductName) {
this.landingPageProductName = landingPageProductName;
}
public PageObjectManager getPageObjectManager() {
return pageObjectManager;
}
public GenericUtils getGenericUtils() {
return genericUtils;
}
public TestBase getTestBase() {
return testBase;
}
}

Ejecución de escenarios

Podemos ejecutar:

  • A través del IDE. En la clase runner RunCucumberTest menú contextual Run As -> JUnitTest. 
  • Por línea de comandos, con maven
C:\desarrollo\workspace\automation>mvn test
[INFO] Scanning for projects...
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.example.cucumber.automation.RunCucumberTest
…
[INFO]
[INFO] Results:
[INFO]
[ERROR] Failures:
[ERROR]   expected: <No data> but was: <Beetroot>
[INFO]
[ERROR] Tests run: 11, Failures: 1, Errors: 0, Skipped: 4

Con cualquiera de las opciones se va ejecutando un browser para cada escenario. En él se ejecutan las pruebas programadas tal y cómo se muestra en el vídeo.

Mejoras en tiempos de ejecución

Por defecto los test de cada escenario de prueba se ejecutan de forma secuencial.  Con JUnit 5 podemos configurar que se ejecuten los escenarios en paralelo. 

Una posibilidad es añadir en el classpath un fichero junit-platform.properties con la siguiente propiedad:

cucumber.execution.parallel.enabled=true

En el vídeo podemos comprobar las diferencias con el comportamiento secuencial. Se ejecutan varias instancias simultáneas del navegador elegido y se realizará en cada una sus acciones, terminando antes la ejecución.

El resto de opciones de configuración se pueden consultar en la documentación.

Informes

Cucumber se puede configurar para obtener informes en distintos formatos. Se realiza mediante la propiedad PLUGIN_PROPERTY_NAME: 

@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "html:target/cucumber.html,json:target/cucumber.json,junit:target/cucumber.xml,"
+ "com.aventstack.extentreports.cucumber.adapter.ExtentCucumberAdapter:")
public class RunCucumberTest {
}
  • html:target/cucumber.html 

Genera un informe en html básico en la carpeta y con el nombre indicado. En él podemos ver estadísticas agregadas sobre los test lanzados. Nos proporciona información de cada feature y step indicado si se ha ejecutado correctamente o ha fallado.

  • json:target/cucumber.json / junit:target/cucumber.xml

Misma información pero en formato json o xml. Se puede utilizar como entrada estructurada para otras herramientas de análisis.

Es recomendable utilizar extend reports. Se activa indicando  ExtentCucumberAdapter. La salida es en formato html y añade la traza de error vinculada al step. También la captura de pantalla del momento previo al fallo. Esta información facilita trazar los errores en entornos reales con muchos escenarios.

Cucumber
Cucumber

Conclusiones

Hemos visto un ejemplo práctico de cómo Cucumber ayuda a automatizar y estructurar las pruebas en un entorno BDD. Especificando el comportamiento en Gherkin todos los implicados en el desarrollo disponen de un lenguaje común y estandarizado para definir los escenarios de prueba. Se puede utilizar en combinación con otros frameworks más orientados a testear funcionalidad como Selenium. Es fácil de integrar con JUnit así como generar informes para tener disponible toda la información acerca de la ejecución.

Los ejemplos de este post están accesibles en el repositorio de github.

Referencias de interés:

https://github.com/cucumber/common

https://github.com/grasshopper7/extentreports-cucumber7-adapter

https://www.selenium.dev/https://rahulshettyacademy.com/

Artículos relacionados

Descubriendo las posibilidades de los componentes web con Polymer

Descubriendo las posibilidades de los componentes web con Polymer

En este post, exploraremos qué son los Web Components, tecnologías estándar de la web que facilitan la creación de componentes reutilizables y encapsulados. Analizaremos cómo Polymer simplifica su uso y promueve las mejores prácticas de desarrollo, proporcionando herramientas y características que facilitan la construcción de

No code

Qué es el No Code: Principales herramientas

La capacidad de crear soluciones tecnológicas sin la necesidad de escribir código se ha convertido en una tendencia cada vez más relevante. Esto se debe en gran parte al surgimiento de herramientas No Code, que permiten a personas con diversos niveles de habilidad técnica dar

Object Pooling

Patrones de diseño en los videojuegos: Object Pooling

El uso de patrones de diseño, como el Object Pooling, es una práctica muy recomendable cuando se quieren realizar desarrollos de software escalables, robustos y que tengan un buen rendimiento. Además, nos puede ayudar a mantener una estructuración de todo el código fuente para ayudar