BDD avec Cucumber

Cucumber est un framework de test dédié à l’écriture de tests fonctionnels dans un style behaviour-driven development (BDD). La description des tests s’effectue au moyen de Gherkin, un langage non-technique et orienté langage naturel afin de s’adresser à toute l’équipe de développement, y compris les analystes métiers. Gherkin supporte une cinquantaine de langues (les mots-clés ont été traduits) ce qui facilite son utilisation par des utilisateurs non-techniques.

Les tests écrits avec Gherkin sont structurés de la façon suivante :

  • la Feature : c’est le plus haut niveau d’un test, qui indique le nom de la fonctionnalité et éventuellement une description textuelle ;
  • le Scenario : il décrit un scénario de test contenant une succession d’étapes et basé sur la syntaxe « Given-When-Then ».

Voici un exemple de scénario (tiré de Wikipedia) :

Scenario: Eric wants to withdraw money from his bank account at an ATM
    Given Eric has a valid Credit or Debit card
    And his account balance is $100
    When he inserts his card
    And withdraws $45
    Then the ATM should return $45
    And his account balance is $55

Atelier : test Cucumber avec Java

Nous allons mettre en pratique Cucumber avec une fonctionnalité simple : le calcul de l’addition d’une commande dans un restaurant. Nous demandons alors à notre business analyst, qui sort d’un restaurant japonais, de nous décrire un scénario de test inspiré en Gherkin :

Feature: Order computation
  To allow a customer to order some dishes.

  Scenario: Order some dishes an compute the order amount
    Given the menu contains the following dishes
      | name        | price |
      | gyoza       | 8.0   |
      | okonomiyaki | 17.0  |
      | yakisoba    | 11.50 |
      | kimchi      | 4.0   |
      | sukiyaki    | 10.25 |
    When the customer orders 2 gyoza
    And  the customer orders 3 kimchi
    And  the customer orders 1 okonomiyaki 
    And  the customer orders 2 sukiyaki
    Then the order amount should be 65.50

Créons un projet Java 8 en incluant les dépendances Maven suivantes :

<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.12</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>info.cukes</groupId>
  <artifactId>cucumber-java</artifactId>
  <version>1.2.3</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>info.cukes</groupId>
  <artifactId>cucumber-junit</artifactId>
  <version>1.2.3</version>
  <scope>test</scope>
</dependency>

Le scénario décrit des données sous forme d’un tableau. Nous isolons une classe métier Dish, avec 2 attributs name et price et implémentons la classe POJO associée :

package restaurant;

public class Dish {
  
  private String name;
  private Float price;
  
  // implement accessors and constructors as needed
}

Implémentons ensuite la fonctionnalité à l’aide de la classe OrderHelper

package restaurant;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

public class OrderHelper {
  private Map<String, Dish> dishes = new HashMap<>();
  
  private Float amount = 0f;
  
  public OrderHelper(final List<Dish> dishes) {
    this.dishes = dishes.stream().collect(Collectors.toMap(Dish::getName, Function.<Dish> identity()));
  }
  
  public void orderDish(String dishName, Integer quantity) {
    Dish dish = dishes.get(dishName);
    Float price = dish.getPrice();
    amount = price * quantity;
  }
  
  public Float getAmount() {
    return amount;
  }

}

Dans le constructeur, nous convertissons la liste de plats dans un dictionnaire permettant d’accéder plus facilement aux plats par leur nom (nous utilisons un stream Java 8).

La méthode orderDish() met à jour le montant de l’addition à chaque fois qu’un plat est commandé dans une certaine quantité. Nous avons volontairement laissé traîner une grossière erreur, afin de provoquer l’échec du test.

Nous pouvons alors procéder à l’écriture du test. Nous créons d’abord une classe RestaurantTest dont le seul rôle est d’indiquer que c’est le test-runner de Cucumber qui sera utilisé avec JUnit :

package restaurant;

import org.junit.runner.RunWith;

import cucumber.api.junit.Cucumber;

@RunWith(Cucumber.class)
public class RestaurantTest {

}

Dans la classe RestaurantSteps, nous décrivons comment les différentes phrases du scénario de test vont être interprétées. Pour cela, nous utilisons des annotations dédiées (@Given, @When, @Then) avec des expressions régulières qui permettent d’identifier les différents paramètres :

package restaurant;

import java.util.List;

import org.junit.Assert;

import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;

public class RestaurantSteps {
  
  OrderHelper orderHelper;
  
  @Given("the menu contains the following dishes$")
  public void the_menu_contains_the_following_dishes(final List<Dish> dishes) {
    orderHelper = new OrderHelper(dishes);
  }
  
  @When("the customer orders (\\d+) (.+)$")
  public void the_customer_orders(final Integer quantity, final String dishName) {
    orderHelper.orderDish(dishName, quantity);
  }
  
  @Then("the order amount should be (\\d+\\.\\d+)$")
  public void the_order_amount_should_be(final Float amount) {
    Assert.assertEquals(amount, orderHelper.getAmount());
  }

}

Remarques :

  • les méthodes peuvent être nommées comme on le souhaite, mais par convention nous utilisons la phrase présente dans l’annotation ;
  • les paramètres doivent apparaître dans le même ordre que dans la phrase de l’annotation, et le type doit être compatible avec les valeurs parsées.

La spécification Gherkin doit être placée dans un fichier d’extension .feature et dans le même package que les classes de test. A partir de la classe de tests, Cucumber se charge de la découverte du fichier .feature et de la classe de steps associée.

L’exécution du test donne le résultat suivant :

cucumber_restau1

Le test étant en erreur, on corrige l’implémentation (l’anomalie se situe à la ligne 21 de la classe OrderHelper) et on relance :

cucumber_restau2

Cucumber a généré les testsuites, testcases et méthodes de tests de telle façon que le déroulement du test soit lisible sous une forme BDD.

Si la syntaxe par annotations paraît trop lourde, Cucumber fournit une extension permettant d’utiliser les expressions lambda. La dépendance suivante doit être ajoutée au projet :

<dependency>
  <groupId>info.cukes</groupId>
  <artifactId>cucumber-java8</artifactId>
  <version>1.2.3</version>
  <scope>test</scope>
</dependency>

Les steps se réécrivent alors de la façon suivante :

package restaurant_java8;

import org.junit.Assert;

import cucumber.api.DataTable;
import cucumber.api.java8.En;
import restaurant.Dish;
import restaurant.OrderHelper;

public class RestaurantJava8Stepdefs implements En {
  
  OrderHelper orderHelper;
  
  public RestaurantJava8Stepdefs() {
    Given("the menu contains the following dishes", (final DataTable dishes) -> {
      orderHelper = new OrderHelper(dishes.asList(Dish.class));
    });
    When("the customer orders (\\d+) (.+)", (final Integer quantity, final String dishName) -> {
      orderHelper.orderDish(dishName, quantity);
    });
    Then("the order amount should be (\\d+\\.\\d+)", (final Float amount) -> {
      Assert.assertEquals(amount, orderHelper.getAmount());
    });
  }
  
}

Le code source de cet atelier est disponible sur GitHub.

Laisser un commentaire