jeudi 16 avril 2015

BDD (Behavior Driven Development) en pratique !

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


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.

3.      Ecriture de classe de configuration

.

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