Icono del sitio Profile Software Services

Cucumber: automatizar y estructurar pruebas en un entorno BDD

cucumber

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

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:

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

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:

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.

# -- 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:

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:

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 {
}

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.

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.

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/

Salir de la versión móvil