Cucumber¶
Cucumber est un framework pour l’implémentation de scénarios de type BDD (Behavior-Driven-Development). Dans ce chapitre, nous verrons en quoi consiste une approche BDD ainsi qu’une introduction à l’automatisation de scénarios avec Cucumber en Java. Enfin nous verrons comment valider une application Web en associant Cucumber et Selenium.
Behavior-Driven-Development (BDD)¶
Le BDD (Behavior-Driven-Development) est une approche proposée par Dan North dans son article Introducing BDD. Le fondement du BDD est moins de raisonner en terme de tests que de comportements d’un système.
Un test fait le plus souvent référence à une interaction entre un utilisateur et un système. Le résultat de cette interaction doit être conforme à une exigence ou une expression de besoins. Le test est un formalisme pour valider le travail de développement.
L’approche BDD oblige à revoir la notion de test et d’expression de besoins. Par exemple, dans une approche agile (qui est à la base de l’approche BDD), une nouvelle fonctionnalité à ajouter dans un système est associée à un ensemble de critères d’acceptation avant que l’équipe ne la développe. Dan North propose de représenter autant que possible ces critères d’acceptation par des scénarios décrivant les comportements attendus du système. Pour faciliter la communication, il propose de suivre un formalisme simple pour écrire ces scénarios : Given, When, Then :
Given some initial context,
When an event occurs,
Then ensure some outcomes.
Soit en français :
Etant donné un contexte initial,
Quand un événement survient,
Alors on s'assure de certains résultats
Note
Le formalisme du BDD reprend le principe du AAA des tests. L’étape Given correspond à la phase Arrange, l’étape When correspond à la phase Act et l’étape When correspond à la phase Assert.
En utilisant ce formalisme, les scénarios deviennent facilement communicables entre les développeurs, les experts fonctionnels et toutes les parties-prenantes dans la création de la solution. Les scénarios deviennent un support à la discussion et à la compréhension des fonctionnalités. Idéalement avec le BDD, la spécification du système est l’ensemble des scénarios définis.
De plus, en utilisant ce formalisme, il est possible de concevoir un outil capable d’analyser les scénarios et de les exécuter. On parle alors de spécification exécutable. Ainsi, les scénarios, bien qu’automatisés, demeurent écrits en langage naturel et sont facilement accessibles à l’ensemble des intervenants du projet.
Il ne faut pas voir le BDD comme une approche technique mais bien plutôt comme une manière d’organiser le développement logiciel différemment en repensant, d’une part, le lien entre expression de besoins et validation et, d’autre part, la communication entre les développeurs et les experts fonctionnels.
Plusieurs framework ont été proposés pour aider à réaliser ce type d’approche : FitNesse, JBehave (proposé par Dan North lui-même) et Cucumber. Cucumber est probablement le plus utilisé actuellement. Il a l’avantage de ne pas être lié à un seul langage de programmation car il a été porté pour plusieurs langages.
Écrire des scénarios avec Cucumber¶
Cucumber permet de rédiger des scénarios en suivant un formalisme appelé le
langage Gherkin. Les scénarios sont regroupés par fonctionnalité (feature).
Ils sont écrits dans un simple fichier texte. Par défaut, Cucumber s’attend à
ce que le fichier porte l’extension .feature
.
Imaginons que nous voulions tester le jeu du pendu (the hangman en anglais).
Nous pourrions écrire les scénarios suivant dans le fichier
PlayHagman.feature
:
Feature: Suggesting a letter
Scenario: Knowing the occurence of one letter in the word
Given the word to be guessed is cucumber
When the player suggests the letter u
Then the letter is found 2 times
Scenario: Knowing the position of all occurences in the word
Given the word to be guessed is cucumber
When the player suggests the letter u
Then the word is -u-u----
Le fichier commence par le mot clé Feature
suivi de deux points et de la
description de la fonctionnalité. Puis viennent les scénarios. Chacun d’entre-eux
commence par Scenario
suivi de deux points et de son nom. Chaque scénario
est constitué d’étapes (steps) commençant par Given
, When``ou ``Then
.
Il est possible de séquencer plusieurs étapes en utilisant And
. Nous pourrions
donc fusionner les deux scénarios précédents en un seul :
Feature: Suggesting a letter
Scenario: Knowing the occurence and position of one letter in the word
Given the word to be guessed is cucumber
When the player suggests the letter u
Then the letter is found 2 times
And the word is -u-u----
La caractère #
peut être utilisé pour signifier un commentaire. Il est
également possible de l’utiliser suivi d’un mot-clé pour donner une
méta-information. Par exemple, Gherkin est disponible dans de nombreuses langues.
Pour préciser la langue du fichier, on utilise #language
:
#language: fr
Fonctionnalité: Proposer une lettre
Scénario: Connaître l'occurrence d'une lettre dans un mot
Etant donné que le mot à trouver est cucumber
Quand le joueur propose la lettre u
Alors la lettre est présente 2 fois
Scénario: Connaître la position de toutes les occurrences dans un mot
Etant donné que le mot à trouver est cucumber
Quand le joueur propose la lettre u
Alors le mot est -u-u----
La méta-information #language: fr
en tête du fichier permet d’écrire les
tests en français.
Chaque fonctionnalité est décrite par des scénarios dans son propre fichier
portant l’extension .feature
.
Automatiser les scénarios en Java¶
Pour poursuivre notre exemple du jeu du pendu, nous partirons du principe
que les développeurs ont créé la classe JeuDuPendu
que nous allons valider
grâce à nos scénarios :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | package dev.gayerie.jeu;
import java.util.Arrays;
public class JeuDuPendu {
private char[] motSecret;
private char[] lettresDecouvertes;
/**
* Crée une instance du jeu en donnant le mot à trouver.
*
* @param motSecret Le mot à découvrir
*/
public JeuDuPendu(String motSecret) {
this.motSecret = motSecret.toCharArray();
this.lettresDecouvertes = new char[this.motSecret.length];
Arrays.fill(this.lettresDecouvertes, '-');
}
/**
* @param lettre La lettre que le joueur propose
* @return le nombre d'occurrences de la lettre dans le mot
*/
public int proposer(char lettre) {
int nbOccurences = 0;
for(int i = 0; i < motSecret.length; ++i) {
if (motSecret[i] == lettre) {
lettresDecouvertes[i] = lettre;
nbOccurences++;
}
}
return nbOccurences;
}
/**
* @return Les lettres découvertes dans le mot.
* Les lettres inconnues sont remplacées par -
* Par exemple : bo-jo--
*/
public String getLettresDecouvertes() {
return String.valueOf(lettresDecouvertes);
}
}
|
Projet de démo
Vous pouvez trouver un exemple plus complet en téléchargeant le projet
cucumber-demo.zip
qui
contient tous les exemples de ce chapitre.
Pour automatiser les scénarios avec Cucumber, le plus simple est de les lancer avec Junit 4. Dans un projet Java géré avec Maven, on commence par ajouter les dépendances nécessaires :
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>5.6.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId>
<version>5.6.0</version>
<scope>test</scope>
</dependency>
Pour exécuter les scénarios, nous avons besoin :
Des scénarios écrits dans des fichiers texte avec l’exentension
.feature
,D’une ou plusieurs classes contenant l’implémentation à exécuter pour chaque étape (step) d’un scénario,
D’une classe permettant de lancer les scénarios avec JUnit.
Les classes d’implémentation des étapes¶
Un scénario est découpé en étapes (steps). Chaque étape doit être associée
à une méthode qui pourra être exécutée. Une telle méthode est publique et est
annotée avec @Given
, @When
ou @Then
. Même si ce n’est pas nécessaire,
vous pouvez utiliser les annotations équivalentes dans la langue de vos scénarios.
(par exemple, Cucumber fournit des annotations @Etantdonné
, @Etantdonnéque
,
@Quand
, @Alors
).
Pour l’exemple du jeu du pendu, voici la classe d’implémentation des étapes :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | package dev.gayerie.jeu;
import static org.junit.Assert.*;
import io.cucumber.java.fr.Alors;
import io.cucumber.java.fr.Etantdonnéque;
import io.cucumber.java.fr.Quand;
public class JeuDuPenduSteps {
private JeuDuPendu jeuDuPendu;
private int nbOccurencesDerniereLettreProposee;
@Etantdonnéque("le mot à trouver est {word}")
public void leMotATrouver(String mot) {
jeuDuPendu = new JeuDuPendu(mot);
}
@Quand("le joueur propose la lettre {word}")
public void proposerLettre(String lettre) {
nbOccurencesDerniereLettreProposee = jeuDuPendu.proposer(lettre.charAt(0));
}
@Alors("la lettre est présente {int} fois")
public void laLettreEstPresente(int nbFois) {
assertEquals(nbFois, nbOccurencesDerniereLettreProposee);
}
@Alors("le mot est {}")
public void leMotEst(String mot) {
assertEquals(mot, jeuDuPendu.getLettresDecouvertes());
}
}
|
Notez que ces méthodes utilisent les annotations pour préciser le texte de
l’étape qu’elles représentent. Il est possible de paramétrer les étapes avec
une notation de la forme {}
donnant le type du paramètre.
Notation |
Description |
---|---|
{int} |
Le paramètre est un entier |
{float} |
Le paramètre est un nombre à virgule |
{word} |
Le paramètre est un mot sans espace |
{string} |
Le paramètre est une chaîne de caractères délimitée par des apostrophes ou des guillemets |
{} |
Le paramètre est une chaîne de caractères et correspond à n’importe quelle valeur |
Par exemple, à la ligne 14 de la classe, on associe la méthode une étape de la forme
le mot à trouver est {word}
. Cucumber se chargera d’extraire le mot apparaissant
dans le scénario pour le passer en paramètre de la méthode.
Note
Si vous préférez, vous pouvez associer les méthodes aux étapes en utilisant des expressions régulières. Les paramètres à extraire correspondent aux groupes définis dans votre expression.
Une classe définissant des méthodes d’étape agit le plus souvent comme une
machine à état. Elle maintient en interne des attributs qui sont modifiés au
fur et à mesure de l’appel aux méthodes. Remarquez que les méthodes annotées
avec @Alors
utilisent des assertions JUnit pour valider l’état courant.
Note
Pour allez plus loin, vous pouvez consulter la documentation officielle :
La classe de lancement JUnit¶
La classe de lancement JUnit est très simple à écrire car elle ne contient aucun test particulier. Grâce à l’annotation @RunWith, elle déclare que le moteur JUnit doit utiliser Cucumber pour se lancer. On peut également utiliser l’annotation @CucumberOptions pour paramétrer l’exécution des scénarios.
1 2 3 4 5 6 7 8 9 10 11 12 | package dev.gayerie.jeu;
import org.junit.runner.RunWith;
import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
@RunWith(Cucumber.class)
@CucumberOptions(strict = true, monochrome = true)
public class JeuDuPenduTest {
}
|
Pour localiser les fichiers .feature
et les classes fournissant
l’implémentation des étapes, la règle par défaut est d’utiliser le même package
que le package de la classe de lancement.
Pour l’exemple ci-dessus, toutes les classes dans le package
dev.gayerie.jeu et définissant des étapes seront utilisées. Dans le répertoire
Maven src/test/resources
, tous les fichiers avec l’extension .feature
présents dans le package dev.gayerie.jeu seront lus et exécutés.
Pour exécuter les scénarios associés, il suffit de lancer cette classe de test dans votre IDE ou de lancer la commande Maven :
mvn test
Intégration avec Selenium¶
Il est relativement simple d’intégrer Selenium dans l’implémentation des étapes. Cela permet d’écrire des scénarios impliquant l’utilisation de navigateur Web.
À titre d’illustration, nous allons reprendre l’exemple du Page Object Model vu au chapitre sur Selenium.
#language: fr
Fonctionnalité: Référencement sur le moteur de recherche DuckDuckGo
Scénario: Le site de cucumber apparaît dans la première page de résultats de DuckDuckGo
Etant donné que l'utilisateur est sur la page d'accueil de DuckDuckGo
Quand il saisit le mot-clé cucumber
Et qu'il lance la recherche
Alors la page de résultats contient un lien sur le site https://cucumber.io
Scénario: Le site de selenium apparaît dans la première page de résultats de DuckDuckGo
Etant donné que l'utilisateur est sur la page d'accueil de DuckDuckGo
Quand il saisit le mot-clé selenium
Et qu'il lance la recherche
Alors la page de résultats contient un lien sur le site https://www.selenium.dev
package dev.gayerie.duckduckgo;
import static org.junit.Assert.*;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import io.cucumber.java.After;
import io.cucumber.java.Before;
import io.cucumber.java.fr.Alors;
import io.cucumber.java.fr.Etantdonnéque;
import io.cucumber.java.fr.Quand;
public class DuckDuckGoSteps {
private WebDriver webDriver;
private HomePage homePage;
private ResultPage resultPage;
@Before
public void createWebDriver() {
webDriver = new ChromeDriver();
}
@After
public void quitWebDriver() {
webDriver.quit();
}
@Etantdonnéque("l'utilisateur est sur la page d'accueil de DuckDuckGo")
public void utilisateurSurLaPageDAccueil() {
homePage = HomePage.openWith(webDriver);
}
@Quand("il saisit le mot-clé {word}")
public void ilSaisit(String motCle) {
assertNotNull("La page de recherche n'est pas disponible", homePage);
homePage.enterKeywords(motCle);
}
@Quand("il lance la recherche")
public void ilLanceLaRecherche() {
assertNotNull("La page de recherche n'est pas disponible", homePage);
resultPage = homePage.clickOnSearch();
homePage = null;
}
@Alors("la page de résultats contient un lien sur le site {}")
public void laPageDeResultatContient(String site) {
assertNotNull("La page de résultats n'est pas disponible", resultPage);
assertTrue("Pas de résultat pour " + site, resultPage.isLinkPresent(site));
}
}
L’API de Cucumber fournit les annotations @Before
et @After
pour déclarer
des méthodes publiques qui seront appelées avant chaque scénario (respectivement
après chaque scénario). Ainsi, il est possible de créer et de libérer
correctement une instance de WebDriver.
La classe de lancement des scénarios est simplement :
package dev.gayerie.duckduckgo;
import org.junit.runner.RunWith;
import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
@RunWith(Cucumber.class)
@CucumberOptions(strict = true, monochrome = true)
public class DuckDuckGoAcceptanceTest {
}
Important
Notez que les classes DuckDuckGoSteps
et DuckDuckGoAcceptanceTest
appartiennent au même package. De même, le fichier DuckDuckGo.feature
doit être placé dans le répertoire src/test/resources
dans le
package dev.gayerie.duckduckgo.
Pour aller plus loin¶
Cucumber dispose de nombreuses fonctionnalités pour permettre de s’adapter à des solutions complexes.
Le plan de scénario¶
Afin d’éviter de répéter des scénarios qui ne varient que sur leurs paramètres en entrée et leurs paramètres en sortie, il est possible de créer un plan de scénario (scenario outline ou scenario template) et de passer les valeurs à utiliser sous la forme d’un tableau d’exemple :
Plan du scénario: Connaître la position d'une lettre
Etant donné que le mot à trouver est cucumber
Quand le joueur propose la lettre <lettre>
Alors le mot est <mot>
Exemples:
| lettre | mot |
| c | c-c----- |
| u | -u-u---- |
| m | ----m--- |
| w | -------- |
Le plan ci-dessus permet d’exécuter quatre scénarios différents en remplaçant
à chaque fois <lettre>
et <mot>
par leur valeur.
Le contexte¶
Le contexte (background) correspond à un ensemble d’étapes à réaliser avant tous les scénarios d’une fonctionnalité. Cela permet d’éviter de copier/coller des étapes dans chaque test qui n’apportent pas une réelle plus-value au lecteur.
Fonctionnalité: Gérer les commandes
Contexte:
Etant donné que l'utilisateur est loggé comme administrateur
Et qu'il existe une commande en attente avec le numéro "XD-1234"
Scénario: Consulter la liste des commandes
Quand l'utilisateur consulte la liste des commandes
Alors il voit la commande avec le numéro "XD-1234"
Scénario: Editer une commande en attente
Quand l'utilisateur consulte la liste des commandes
Et qu'il édite la commande avec le numéro "XD-1234"
Alors il peut annuler la commande
Les tags¶
Il est possible d’ajouter des tags sur des scénarios afin de filtrer leur
exécution. Par exemple, une partie des scénarios ne sont peut-être pas
automatisables pour des raisons de coûts ou de difficultés techniques. Il est
possible de les marquer par un tag @manuel
.
@manuel
Scenario: Gérer l'annulation d'une commande
Etant donné qu'une commande n'a pas encore été livrée
Quand l'utilisateur consulte la commande
Et qu'il la supprime
Alors le service commande reçoit une notification d'annulation
Dans la classe de lancement des scénarios, on peut spécifier de n’exécuter
que les scénarios qui ne disposent pas du tag @manuel
:
1 2 3 4 5 6 7 8 9 10 11 12 | package dev.gayerie.jeu;
import org.junit.runner.RunWith;
import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
@RunWith(Cucumber.class)
@CucumberOptions(strict = true, monochrome = true, tags = "not @manuel")
public class JeuDuPenduTest {
}
|
Pour plus d’information, reportez-vous à la documentation sur les tags :
L’injection de dépendances¶
Créer des scénarios à l’échelle d’une application signifie créer un large panel d’étapes avec leur méthode Java associée. Il est plus simple de répartir ces méthodes dans plusieurs classes pour des questions de lisibilité et de maintenance. Il est alors courant qu’un même scénario requière l’utilisation d’étapes qui sont implémentées par différentes classes. Les instances de ces classes devront la plupart du temps partager un état. Pensez, par exemple, à un scénario impliquant une interface Web, il est nécessaire de partager au minimum l’instance du WebDriver Selenium qui permet de piloter le navigateur Web.
Cucumber propose des solutions d’injection de dépendances se basant sur différents frameworks : Spring, Guice, Pico Container…
Pour en savoir plus, reportez-vous à la documentation officielle :