Une
des problématiques de développement logiciel est le manque de communication
entre les différents participants. Pire, dans la plupart des cas, ils emploient
des vocabulaires différents pour parler de la même chose. En effet, généralement
dans les projets de développement, les experts du métier rédigent des
spécifications qui sont ensuite transmises à l’équipe de développement. Les
développeurs relisent les spécifications, comprennent et interprètent à leur
façon avant de commencer à coder selon leur compréhension. L’équipe de
développement pourrait très bien mal interpréter certains concepts spécifiés. Lorsqu'on
interroge un expert métier sur une notion présente dans le code du domaine, et
qu'on s'aperçoit qu'elle est en total décalage avec la réalité, même si
l'application semble se comporter correctement pour l'utilisateur. Il peut
s'agir d'une mauvaise interprétation ou d'une supposition hasardeuse de la part
d'un développeur. Le BBD nous aide à remédier à cette problématique
d’interprétation des termes, des concepts liés au domaine fonctionnel.
BDD
(Behavior Driven Development) : c’est une pratique de développement qui a
pour objectif de mettre en place un vocabulaire, un langage commun et
généralisé pour décrire ce que doit faire notre application. Cela se fait par
une collaboration étroite entre les experts du domaine fonctionnel/ métiers,
concepteurs et développeurs pour construire du code qui reflète la réalité du
domaine fonctionnel et permet de résoudre de vraies problématiques métier. Il
s’agit d’écrire des tests qui décrivent le comportement attendu du système et
que tout le monde peux comprendre. Le code n’est donc plus compréhensible
uniquement par des développeurs, mais sous forme de scénario compréhensible par
toutes les personnes impliquées dans le projet.
L’objectif
visé est de rendre le code à l’image du domaine fonctionnel, clair et
compréhensible, et donner tout de suite une vision d'ensemble à celui qui y
jette un œil. Malheureusement ce n'est pas toujours le cas, souvent à cause
d'une représentation brouillonne du domaine dans le code due à un manque de
dialogue avec les experts métier. De plus il est facile de se perdre en créant
toujours plus de helpers, de services et autres gestionnaires qui embrouillent
la compréhension du domaine.
C'est un peu abstrait tout ça !
Par où commencer ??? ===> Création
de Story
Nous
avons vu précédemment que le comportement de l’application doit être défini en
collaboration étroite avec les experts métier. Cette collaboration peut se
matérialiser dans un premier temps par une demande en terme des besoins (une
story/histoire) de l’expert métier. Chaque demande est priorisée. Comme le travail
d’un expert du domaine fonctionnel n’est pas d’écrire des tests unitaires, il
faudra trouver un langage simple avec un nombre de vocabulaire limité,
compréhensible par tous et qui pourra être utilisé à la fois par les expert pour
exprimer leur besoins fonctionnels (une story/histoire) et par les développeurs
pour effectuer leur tests.
BDD utilise le langage Gherkin, un formalisme ultra
simple qui permet de modéliser une story. Gherkin est un langage
"Spécifique à un Domaine fonctionnel, et lisible par un Fonctionnel"
et a été imaginé spécialement pour la description de comportements. Il vous
offre la possibilité d'abstraire les détails d'implémentation de vos tests de
comportement. L’expert métier commence par donner une description de ce qui est
attendu par la story puis décrit en « détail » en utilisant au plus 3
vocabulaires du langage Gherkin : En tant que (As role), je veux (I want),
Pour/A fin (In order to) :
La
description d’une Story met d’abord en place un contexte (En tant que …). Puis
va nous demander une fonctionnalité (Je veux, souhaite …). Et enfin nous
explique quelle est la finalité de cette Story (Afin, Pour …)
Exemples
:
- En tant qu'étudiant, Je
veux m'inscrire à une formation Afin d'obtenir le diplôme.
- En tant que voyageur, Je
veux réserver un billet de train Afin d'aller voir ma mère.
- En tant qu'opérateur Je
veux créer un compte pour un client Afin de recevoir son
argent.
- En tant qu'organisateur, Je
veux connaître le nombre de personnes inscrites à la conférence Afin
de choisir la salle adéquate.
- En tant que client de la banque, Je
veux pouvoir créditer mon compte, Afin d’augmenter mon solde.
Une
fois les demandes formalisées sous forme de Story, il faudra écrire les
scénarios de chaque Story.
Ecriture
des scénarios
En
complément des demandes en termes des story, l’expert métier va devoir
réfléchir à des cas de test. De la même manière que lorsqu’il rédige un cahier
de recette, il va pouvoir mettre à l’épreuve sa demande et détailler son
besoin. De la même manière qu’une story, les cas de test sont formalisés avec
un langage Gherkin composé de mot
clés :
Un
scénario met en évidence la réponse que doit fournir le système face à un événement
compte tenu d’un contexte initial. Chaque scénario est constitué d’une
description et d’un ensemble d’étape (Step). Il existe 3 étapes :
- Given : qui permet de définir et de
construire le contexte dans lequel le scénario va se dérouler,
- When : soit de provoquer des
événements ou des actions sollicitant le système When,
- soit de vérifier que le comportement
attendu a bien eu lieu Then; c’est généralement à ces
étapes que l’on retrouvera les assertions.
Une story est constituée d’un
ensemble de scénarios nous permettant de tester le cas passant (Tout se passe
bien, c’est le cas le plus classique) ou
non passant.
Exemple
1 : Story « En tant que client de la banque, Je veux pouvoir
créditer mon compte, Afin d’augmenter mon solde »
On
pourra trouver le scénario:
Scénario
1 : créditer un compte
Given je dispose de 20 euros sur mon
compte bancaire
When je crédite mon compte de 10
euros
Then mon solde devrait être de 30
euros.
Exemple
2 : Story « En tant qu’utilisateur, je veux me connecter à google afin
d’accéder à tous mes services en lignes »
Scénario
1 : Accéder à la page de connexion
Given L’utilisateur n’est pas connecté
And L’utilisateur est sur la page
d’accueil google
When L’utilisateur demande à se
connecter
Then La page de connexion est affichée
Scénario
2 : Se connecter
Given L’utilisateur n’est pas connecté
And L’utilisateur est sur la page de
connexion
When L’utilisateur saisi son
identifiant
And L’utilisateur saisi son mot de
passe
And L’utilisateur clique sur «
Connexion »
Then La page d’accueil google
s’affiche en <30s
And L’identifiant de l’utilisateur
est affiché à droite dans le bandeau.
Exemple
3 : Story « En tant que client, je veux pouvoir acheter un billet de
train afin de me rendre à ma destination»
Scénario
1 : Acheter un billet pour Bamako avec ma carte bancaire et avec assez d’argent
sur mon compte
Given
il reste de la place dans le train pour Bamako
And ma carte bancaire est valide
And j’ai suffisamment d’argent sur
mon compte
When je valide l’achat d’un billet
pour Londres avec ma carte bancaire
Then je reçois une confirmation de la
transaction
And j’obtiens 1 billet pour Bamako
And j’obtiens le reçu de ma carte
bancaire
And mon compte est débité du montant
du billet
Scénario
2 : Acheter un billet pour Bamako avec ma carte bancaire et avec peu d’argent
sur mon compte
Given il reste de la place dans le
train pour Londres
And ma carte bancaire est valide
And je n’ai pas suffisamment
d’argent sur mon compte
When je valide l’achat d’un billet
pour Londres avec ma carte bancaire
Then je reçois un message m’indiquant
que la transaction a échoué
BDD en pratique avec Jbehave
Historiquement
le premier frameworks à avoir vu le jour pour BDD fut Jbehave.
Pour
commencer, vous pouvez installer le plugin Jbehave. C’est
simple pour eclipse :
Help > Install New Software...
Add the new site location http://jbehave.org/reference/eclipse/updates/
Select JBehave Eclipse feature and follow standard
Eclipse installation procedure
Dépendance Maven :
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jbehave.version>3.9</jbehave.version>
</properties>
<dependency>
<groupId>org.jbehave</groupId>
<artifactId>jbehave-core</artifactId>
<version>${jbehave.version}</version>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>jbehave-junit-runner</artifactId>
<version>1.1.2</version>
<scope>provided</scope>
</dependency>
Les scénarios d'une Story sont décrits dans un fichier texte (une Story par fichier). Par où commencer ? La méthodologie BDD préconise de prioriser en choisissant la Story qui se porte sur le comportement le plus important de l’application. La décision de priorisation est prise en concertation avec le chargé fonctionnel. Le choix se porte le plus souvent sur la valeur business la plus importante. La question simple à se poser, est : Quelle est la fonctionnalité la plus importante qui manque actuellement à mon application ?
La
structure du fichier est la suivante :
Le fichier texte définit nos jeux de tests à
vérifier, définissant donc nos critères d’acceptabilité. Usuellement, le fichier
texte porte l’extension « .Story », mais il est également possible de customiser
via la classe «StoryPathResolver» qui
permet de rechercher les fichiers histoires d’après l’extension.
Jbehave
vous permet tout simplement de mapper le fichier contenant les scénarios d’une
story sur une classe de test Java exécutable. Les étapes à suivre sont :
-
Ecriture
des scénarios de chaque story dans un fichier texte
-
Ecriture
de classes POJO qui mappent sur les scénarios
-
Ecriture
de la classe de configuration permettant d’exécuter les scénarios.
Un
exemple I
: DAB Distributeur Automatique de Billet
1.
Ecriture
des scénarios
Pour une raison de simplicité, nous allons voir deux Stories d'une application de gestion de DAB. Une Story pour la création de compte bancaire et une autre pour le retrait d’argent depuis le distributeur de billet.
Story 1 : Création d'un compte bancaire
Story 2 : Retrait d'argent depuis un distributeur de billet
Jbehave
propose une table contenant une liste de paramètre afin de pouvoir exécuter le
même scénario plusieurs fois. Chaque ligne de la table représente un jeu de
données à exécuter en séquence pour tester différentes déclinaison du scénario.
Dans cet exemple, le scénario sera effectué deux fois avec comme paramètre
respectif la ligne.
2.
Ecriture
de classe de mapping
La classe de mapping Story Création de compte bancaire
package com.larbotech.jbehave;
import junit.framework.Assert;
import org.jbehave.core.annotations.Given;
import org.jbehave.core.annotations.Then;
import org.jbehave.core.annotations.When;
/**
* Created by mtoure on 12/04/15.
*/
public class CompteBancaireSteps {
private CompteBancaire compteBancaire;
@When("Je crée un nouveau compte")
public void creationCompte() {
compteBancaire = new CompteBancaire();
}
@Then("J'obtiens un compte bancaire initialisé à zero comportant un numéro à 4 chiffres")
public void compteCree() {
Assert.assertEquals(compteBancaire.getSolde(), 0);
Assert.assertEquals(String.valueOf(compteBancaire.getNumero()).length(), 4);
}
@Given("Le solde du compte est à zero")
public void soldeZero() {
Assert.assertEquals(compteBancaire.getSolde(), 0);
}
@When("J'ajoute 100 euros")
public void crediterCompte() {
compteBancaire.crediter(100);
}
@Then("Le solde du compte indique 100 euros")
public void compteCredite() {
Assert.assertEquals(compteBancaire.getSolde(), 100);
}
}
La classe mapping Story Retrait d'argent depuis un distributeur de billet
package com.larbotech.jbehave;
import org.jbehave.core.annotations.Given;
import org.jbehave.core.annotations.Named;
import org.jbehave.core.annotations.Then;
import org.jbehave.core.annotations.When;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
public class DABScenarioSteps {
private DistributeurAutomatiqueBillets dab;
private CompteBancaire compteBancaire;
private CarteBancaire carteBancaire;
private int argent;
@Given("le compte du client est crédité d'un montant <solde>")
public void creerCompteBancaire(@Named("solde") int solde) {
compteBancaire = new CompteBancaire(solde);
}
@When("le client dipose d'une carte bancaire")
public void creerCarteBancaire() {
carteBancaire = new CarteBancaire(compteBancaire);
}
@When("le distributeur contient un montant <fonds>")
public void creerDistributeurBillet(@Named("fonds") int fonds) {
dab = new DistributeurAutomatiqueBillets(fonds);
}
@When("le client demande un montant <montant>")
public void retirer(@Named("montant") int montant) {
argent = dab.retrait(carteBancaire, montant);
}
@Then("le distributeur délivre un montant <montant_delivre>")
public void checkMontantDelivreParDistributeur(@Named("montant_delivre") int montant) {
assertThat(argent, is(montant));
}
@Then("le nouveau solde du compte <nouveau_solde>")
public void checkSoldeCompteApresRetrait(@Named("nouveau_solde") int solde) {
assertThat(solde, is(carteBancaire.getCompteBancaire().getSolde()));
}
}
3.
Ecriture
de classe de configuration
3.
Ecriture
de classe de configuration
Pour finir, vous avez la possibilité d'exécuter plusieurs Stories :
La classe de configure Story Création de compte bancaire
package com.larbotech.jbehave;
import static org.jbehave.core.reporters.Format.CONSOLE;
import static org.jbehave.core.reporters.Format.HTML_TEMPLATE;
import java.util.List;
import org.jbehave.core.configuration.Configuration;
import org.jbehave.core.configuration.MostUsefulConfiguration;
import org.jbehave.core.io.LoadFromClasspath;
import org.jbehave.core.junit.JUnitStory;
import org.jbehave.core.reporters.StoryReporterBuilder;
import org.jbehave.core.steps.CandidateSteps;
import org.jbehave.core.steps.InstanceStepsFactory;
public class CompteBancaireScenarios extends JUnitStory {
// Here we specify the configuration, starting from default MostUsefulConfiguration, and changing only what is needed
@Override
public Configuration configuration() {
StoryReporterBuilder storyReporter = //
new StoryReporterBuilder() //
.withDefaultFormats() //
.withFormats(CONSOLE, //
HTML_TEMPLATE) //
.withFailureTrace(true) //
.withFailureTraceCompression(true)
.withRelativeDirectory("jbehave-report")//
;
return new MostUsefulConfiguration()
// where to find the stories
.useStoryLoader(new LoadFromClasspath(this.getClass()))
.useStoryReporterBuilder(storyReporter);
// CONSOLE and TXT reporting
// .useStoryReporterBuilder(new StoryReporterBuilder().withDefaultFormats().withFormats(StoryReporterBuilder.Format.CONSOLE, StoryReporterBuilder.Format.TXT, StoryReporterBuilder.Format.HTML, StoryReporterBuilder.Format.XML));
}
// Here we specify the steps classes
@Override
public List<CandidateSteps> candidateSteps() {
// varargs, can have more that one steps classes
return new InstanceStepsFactory(configuration(), new CompteBancaireSteps()).createCandidateSteps();
}
}
La classe de configuration Story Retrait depuis un distributeur de billet
package com.larbotech.jbehave;
import static org.jbehave.core.reporters.Format.CONSOLE;
import static org.jbehave.core.reporters.Format.HTML_TEMPLATE;
import java.util.List;
import org.jbehave.core.configuration.Configuration;
import org.jbehave.core.configuration.MostUsefulConfiguration;
import org.jbehave.core.io.LoadFromClasspath;
import org.jbehave.core.junit.JUnitStory;
import org.jbehave.core.reporters.StoryReporterBuilder;
import org.jbehave.core.steps.CandidateSteps;
import org.jbehave.core.steps.InstanceStepsFactory;
public class DABScenario extends JUnitStory {
// Here we specify the configuration, starting from default MostUsefulConfiguration, and changing only what is needed
@Override
public Configuration configuration() {
StoryReporterBuilder storyReporter = //
new StoryReporterBuilder() //
.withDefaultFormats() //
.withFormats(CONSOLE, //
HTML_TEMPLATE) //
.withFailureTrace(true) //
.withFailureTraceCompression(true) //
;
return new MostUsefulConfiguration()
// where to find the stories
.useStoryLoader(new LoadFromClasspath(this.getClass()))
.useStoryReporterBuilder(storyReporter);
// CONSOLE and TXT reporting
// .useStoryReporterBuilder(new StoryReporterBuilder().withDefaultFormats().withFormats(StoryReporterBuilder.Format.CONSOLE, StoryReporterBuilder.Format.TXT, StoryReporterBuilder.Format.HTML, StoryReporterBuilder.Format.XML));
}
// Here we specify the steps classes
@Override
public List<CandidateSteps> candidateSteps() {
// varargs, can have more that one steps classes
return new InstanceStepsFactory(configuration(), new DABScenarioSteps()).createCandidateSteps();
}
//
}
Un
exemple II : Inscription de client sur un site eCommerce
Afin
de pouvoir passer sa commande, un nouveau
client doit pouvoir s’inscrire sur le site internet en renseignant ses
informations personnelles. Une fois identifié par le système via son nom, prénom, adresse
mail ainsi que son mode passe, un client peut parcourir et choisir des
articles, qu’il souhaite acheter.
1.
Ecriture
des scénarios
2.
Ecriture
de classe de mapping
package com.larbotech.jbehave;
import junit.framework.Assert;
import org.jbehave.core.annotations.Alias;
import org.jbehave.core.annotations.Given;
import org.jbehave.core.annotations.Named;
import org.jbehave.core.annotations.Then;
import org.jbehave.core.annotations.When;
import com.larbotech.dao.ClientDao;
/**
* Created by mtoure on 12/04/15.
*/
public class InscriptionClientSteps {
private Client client;
private ClientDao clientDao = new ClientDao();
@Given("Un nouveau client")
@Alias("Un client exitant")
public void creationCompte() {
client = new Client();
}
@When("le client entre ces informations <nom>, <prenom>, <email>, <password>")
@Alias("le client entre ces informations larbo, Jean, larbo.jean@gmail.com, secret41")
public void saisirInformationsClient(@Named("nom") String nom, @Named("prenom") String prenom,
@Named("email") String email,@Named("password") String password) {
client.setNom(nom);
client.setPrenom(prenom);
client.setMail(email);
client.setPassword(password);
}
@When("le client valide ses informations")
public void submit() {
clientDao.insert(client);
}
@Then("le compte client est crée")
public void clientCree() {
Assert.assertTrue(client.getId() != null && client.getId()>0);
Assert.assertTrue(clientDao.find(client) != null);
}
@Then("le client n'est pas crée")
public void clientNonCree() {
Assert.assertTrue(client.getId() == null);
}
@Then(" le client est invalide")
public void informationsManquantes() {
Assert.assertFalse(client.estValide());
}
}
Remarquons l’annotation @Alias. Elle permet de spécifier deux lignes de texte d’un même scénario ou presque ! Cela permet par exemple de ne pas réécrire les mêmes actions dans deux étapes (Given, When, Then) différentes.
.
package com.larbotech.jbehave;
import static org.jbehave.core.reporters.Format.CONSOLE;
import static org.jbehave.core.reporters.Format.HTML_TEMPLATE;
import java.util.List;
import org.jbehave.core.configuration.Configuration;
import org.jbehave.core.configuration.MostUsefulConfiguration;
import org.jbehave.core.io.LoadFromClasspath;
import org.jbehave.core.junit.JUnitStory;
import org.jbehave.core.reporters.StoryReporterBuilder;
import org.jbehave.core.steps.CandidateSteps;
import org.jbehave.core.steps.InstanceStepsFactory;
public class InscriptionClientScenarios extends JUnitStory {
// Here we specify the configuration, starting from default MostUsefulConfiguration, and changing only what is needed
@Override
public Configuration configuration() {
StoryReporterBuilder storyReporter = //
new StoryReporterBuilder() //
.withDefaultFormats() //
.withFormats(CONSOLE, //
HTML_TEMPLATE) //
.withFailureTrace(true) //
.withFailureTraceCompression(true)
.withRelativeDirectory("jbehave-report")//
;
return new MostUsefulConfiguration()
// where to find the stories
.useStoryLoader(new LoadFromClasspath(this.getClass()))
.useStoryReporterBuilder(storyReporter);
// CONSOLE and TXT reporting
// .useStoryReporterBuilder(new StoryReporterBuilder().withDefaultFormats().withFormats(StoryReporterBuilder.Format.CONSOLE, StoryReporterBuilder.Format.TXT, StoryReporterBuilder.Format.HTML, StoryReporterBuilder.Format.XML));
}
// Here we specify the steps classes
@Override
public List<CandidateSteps> candidateSteps() {
// varargs, can have more that one steps classes
return new InstanceStepsFactory(configuration(), new InscriptionClientSteps()).createCandidateSteps();
}
}
Pour finir, vous avez la possibilité d'exécuter plusieurs Stories :
package com.larbotech.jbehave; import static org.jbehave.core.io.CodeLocations.codeLocationFromClass; import static org.jbehave.core.reporters.Format.CONSOLE; import static org.jbehave.core.reporters.Format.HTML_TEMPLATE; import java.net.URL; import java.util.List; import org.jbehave.core.configuration.Configuration; import org.jbehave.core.configuration.MostUsefulConfiguration; import org.jbehave.core.io.LoadFromClasspath; import org.jbehave.core.io.StoryFinder; import org.jbehave.core.junit.JUnitStories; import org.jbehave.core.reporters.StoryReporterBuilder; import org.jbehave.core.steps.InjectableStepsFactory; import org.jbehave.core.steps.InstanceStepsFactory; import org.junit.runner.RunWith; import de.codecentric.jbehave.junit.monitoring.JUnitReportingRunner; @RunWith(JUnitReportingRunner.class) public class AllStoriesTest extends JUnitStories { public AllStoriesTest() { configuredEmbedder()// .embedderControls()// .doGenerateViewAfterStories(true)// .doIgnoreFailureInStories(false)// .doIgnoreFailureInView(true)// .doVerboseFailures(true)// .useThreads(2)// .useStoryTimeoutInSecs(6000); } @Override public Configuration configuration() { StoryReporterBuilder storyReporter = // new StoryReporterBuilder() // .withDefaultFormats() // .withFormats(CONSOLE, // HTML_TEMPLATE) // .withFailureTrace(true) // .withFailureTraceCompression(true) // ; return new MostUsefulConfiguration() // where to find the stories .useStoryLoader(new LoadFromClasspath(this.getClass())) .useStoryReporterBuilder(storyReporter); // CONSOLE and TXT reporting // .useStoryReporterBuilder(new StoryReporterBuilder().withDefaultFormats().withFormats(StoryReporterBuilder.Format.CONSOLE, StoryReporterBuilder.Format.TXT, StoryReporterBuilder.Format.HTML, StoryReporterBuilder.Format.XML)); } @Override protected List<String> storyPaths() { URL searchInURL = codeLocationFromClass(this.getClass()); return new StoryFinder().findPaths(searchInURL, "**/*.story", ""); //return Arrays.asList("de/codecentric/simplejbehave/Math.story"); } /**SpringStepsFactory pour spring : annoter des composant avec un custom annotation (stepComponent) * */ @Override public InjectableStepsFactory stepsFactory() { return new InstanceStepsFactory(configuration(), new CompteBancaireSteps(), new DABScenarioSteps(), new InscriptionClientSteps()); } }
Aucun commentaire:
Enregistrer un commentaire