JBehave, BDD et test d’acceptation/acceptance: Démo pratique en 20 min

blog_jbehave_bdd_titre1  blog_jbehave_jira_titre1b blog_bdd_titre2

 

Nous nous intéressons dans ce billet à l'utilisation des tests d'acceptation et le concept BDD (Behaviour Driven Development) implémenté par le framework JBehave.

Nous le pratiquons au cas particulier d'une entreprise morale ayant plusieurs établissements souhaitant ajuster automatiquement l'effectif global en fonction du turn-over effectif dans ses divers établissements (simpliste pour que le billet reste accessible).

Notre démo traite le scénario suivant avec un langage (naturel) d'acceptation proche de celui de la maîtrise d'ouvrage (MOA).

Voici donc les étapes du scénario inspiré de mon article sur design pattern observer.

Scénario de test ou user story (US)

Voici les étapes de notre scénario de test:

Etant donné une entreprise nommée FanDeFleurs (effectif global à zéro (si valeur non nulle il suffit d'adapter la suite)).

Et étant donné un premier établissement nommé FanDeFleursBleus (nbEffectif à zéro).

Et étant donné un second établissement nommé FanDeFleursViolettes (nbEffectif à zéro).

Lorsque l'établissement FanDeFleursBleus embauche un salarié.

Alors l'effectif global de l'entreprise FanDeFleurs s'incrémente de 1 (donc égal à 1 (ou valeur+=1)).

Lorsque l'établissement FanDeFleursBleus embauche un second salarié.

Alors l'effectif global de l'entreprise FanDeFleurs devient égal à 2.

Lorsque l'établissement FanDeFleursViolettes embauche un salarié.

Alors l'effectif global de l'entreprise FanDeFleurs devient égal à 3.

Lorsqu'un salarié quitte l'un des deux établissements.

Alors l'effectif global de l'entreprise FanDefleurs redevient égal à 2.

 

Le test JUnit/JBehave donné plus loin doit correspondre à ce scénario.

 

Et contrairement à nos coutumes, commençons par la fin, c'est à dire le rapport généré par JBehave.

Rapport JBehave

Et le test doit afficher ce genre de rapport (html ou autre):

 

Feature: le design pattern observer en action

Narrative:
In order to tester l'impact sur l'effectif global de l'entreprise en cas de variation dans ses établissements
As a anonyme
I want to suivre la variation de l'effectif global quand les effectifs des établissements changent
Scenario: Embaucher plusieurs salaries puis tester le cas un salarié quitte l'établissement

Given Une entreprise nommee fanDeFleurs avec effectifGlobal=0
And Un etablissement nomme fanDeFleursBleus avec effectif=0
And Un etablissement nomme fanDeFleursViolettes avec effectif=0
When Un etablissement fanDeFleursBleus embauche 1 salarie
When Etablissement fanDeFleursViolettes embauche 1 salarie
When Etablissement fanDeFleursBleus embauche un second salarie
Then Le nombre d'effectifGlobal de l'entreprise fanDeFleurs passe a 3
When Un salarie quitte l'etablissement fanDeFleursBleus
Then Le nombre d'effectifGlobal de l'entreprise fanDeFleurs passe a 2

 

 

Concept BBD: C'est quoi?

 

Avant de passer à la pratique, arrêtons nous deux secondes sur le concept BDD.

Pour dire simplement et rapidement: TDD (Test Driven Development) s'est enrichi avec le BDD.

Le BDD est une extension du TDD permettant via des scénarios écrits en langage naturel et compréhensible de tester le logiciel par tous les acteurs/utilisateurs du projet.

C'est à dire, le BDD permet de vérifier, en langage naturel, les comportements attendus de l'application et ce de manière compréhensible pour chaque acteur du projet.

Il résulte que le logiciel possède réellement une valeur ajoutée métier clairement identifiable.

Néanmoins un seul bémol, la courbe d'apprentissage du framework est encore un peu raide mais une bonne organisation permettra d'automatiser une grande partie des tests BDD de manière transverse aux projets.

Enfin rappelons que les stories (au sens BDD) doivent être décrites de manière SMART (Specific, Measurable, Achievable, Relevant & Timeboxed).

 

Démo

Le model du projet démo contient trois objets. Les mêmes que ceux indiqués dans l'article sur design pattern observer:

Classe POJO Entreprise:

 

Classe POJO Etablissement

 

Classe POJO Effectif:

 

 

Test JBehave: Les étapes

 

Etape 1: Configurer le pom du projet démo :

 

Les dépendances nécessaires sont:

 

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>VOTRE GROUPID</groupId>
    <artifactId>VOTRE ARTIFACTID</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
        <jbehave.version>4.0.4</jbehave.version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.0.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jbehave</groupId>
            <artifactId>jbehave-core</artifactId>
            <version>${jbehave.version}</version>
        </dependency>
        <dependency>
            <groupId>org.jbehave</groupId>
            <artifactId>jbehave-spring</artifactId>
            <version>${jbehave.version}</version>
        </dependency>
        <dependency>
            <groupId>org.jbehave</groupId>
            <artifactId>jbehave-gherkin</artifactId>
            <version>${jbehave.version}</version>

        </dependency>

    </dependencies>

</project>
pom projet demo

 

 

Etape 2: Configurer le framework JBehave (une seule fois)

Maintenant que les dépendances de la version 4.0.4 de jbehave avec spring 4 ( ou spring-boot 1.3) sont en place, nous écrivons un peu de code pour
configurer globalement JBehave.

Ceci est réalisé via une classe abstraite nommée  SpringJBeahveAbstractCfgStory:

 


import static org.jbehave.core.reporters.Format.HTML;
import static org.jbehave.core.reporters.Format.TXT;

import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

import org.jbehave.core.configuration.Configuration;
import org.jbehave.core.configuration.Keywords;
import org.jbehave.core.configuration.MostUsefulConfiguration;
import org.jbehave.core.embedder.Embedder;
import org.jbehave.core.embedder.EmbedderControls;
import org.jbehave.core.i18n.LocalizedKeywords;
import org.jbehave.core.io.LoadFromClasspath;
import org.jbehave.core.io.StoryLoader;
import org.jbehave.core.io.StoryPathResolver;
import org.jbehave.core.io.UnderscoredCamelCaseResolver;
import org.jbehave.core.junit.JUnitStories;
import org.jbehave.core.reporters.FilePrintStreamFactory.ResolveToPackagedName;
import org.jbehave.core.reporters.Format;
import org.jbehave.core.reporters.StoryReporterBuilder;
import org.jbehave.core.steps.InjectableStepsFactory;
import org.jbehave.core.steps.ParameterControls;
import org.jbehave.core.steps.ParameterConverters;
import org.jbehave.core.steps.ParameterConverters.ParameterConverter;
import org.jbehave.core.steps.spring.SpringStepsFactory;
import org.springframework.context.ApplicationContext;


public abstract class SpringJBehaveAbstractCfgStory extends JUnitStories  {

    private static final String STORY_TIMEOUT_STR = "600";
    
    protected abstract ApplicationContext getApplicationContext();

    protected abstract URL urlCodeLocationFromClass();
        
    public SpringJBehaveAbstractCfgStory() {
        super();
        
        Embedder embedder = new Embedder();
        embedder.useEmbedderControls(embedderControls());
        embedder.useMetaFilters(Arrays.asList("-skip"));
        useEmbedder(embedder);
    }
    @Override
    public Configuration configuration() {	
    	Keywords keywords = new LocalizedKeywords(Locale.FRANCE);
        return new MostUsefulConfiguration()
                .useStoryPathResolver(this.storyPathResolver())
                .useStoryLoader(this.storyLoader())
                .useStoryReporterBuilder(this.storyReporterBuilder())
                .useParameterControls(this.parameterControls())
                .useParameterConverters(new ParameterConverters())     
                .useParameterConverters(
                        new ParameterConverters().addConverters(customConverters(keywords)))
                ;
    }
   
	@Override
    public InjectableStepsFactory stepsFactory() {
        return new SpringStepsFactory(configuration(), getApplicationContext());
    }

    protected StoryPathResolver storyPathResolver() {
        return new UnderscoredCamelCaseResolver();
    }

    protected StoryLoader storyLoader() {
        return new LoadFromClasspath();
    }

    protected StoryReporterBuilder storyReporterBuilder() {
        return new StoryReporterBuilder()
                .withCodeLocation(urlCodeLocationFromClass())
                .withPathResolver(new ResolveToPackagedName())
                .withFailureTrace(true)
                .withFormats(Format.CONSOLE, TXT, HTML);
    }
    
    protected ParameterControls parameterControls() {
    	return new ParameterControls()
    			.useDelimiterNamedParameters(true);
    }

    protected  ParameterConverter[] customConverters(Keywords keywords) {
        List<ParameterConverter> converters = new ArrayList<>();
        //converters.add(new NumberConverter(NumberFormat.getInstance(Locale.FRANCE))); 
        return converters.toArray(new ParameterConverter[converters.size()]);
    }
    
    protected EmbedderControls embedderControls() {
        return new EmbedderControls()
                .doIgnoreFailureInView(true)
                .useStoryTimeouts(STORY_TIMEOUT_STR);
    }
}

 

Etape 3

Ecrire le scénario du premier test story

Feature: le design pattern observer en action

Narrative:
In order to tester l'impact sur l'effectif global de l'entreprise en cas de variation dans l'un des etablissements
As a anonyme
I want to suivre l'effectif global quand les divers effectifs des etablissements varient

Scenario: Embaucher un salarie dans etablissement et voir l'effectif global! 


Given Etablissement nomme fanDeFleursBleus avec effectif initial=<effEtab1>
When Etablissement fanDeFleursBleus embauche <effEtab1Add> salarie
Then Le nombre d'effectifGlobal de l'entreprise fanDeFleurs passe a <effGlobal>


Examples:
stories/designobserver/observerentreprise.txt

Etape 4

Ecrire la fixture du premier test (traduit l'user story en code)

 

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 org.springframework.stereotype.Component;

import fr.netapsys.abdou.springbootjbehave.model.Entreprise;
import fr.netapsys.abdou.springbootjbehave.model.Etablissement;

import static org.junit.Assert.*;


@Component
public class DesignObserverStepFixture {

	private Entreprise entFanDeFleurs  				= new Entreprise();
	private Etablissement etabFanDeFleursBleus 		= new Etablissement();
	
	@Given("Etablissement nomme fanDeFleursBleus avec effectif initial=<effEtab1>")
	public void givenEtabfanDeFleursBleus(@Named("effEtab1") int effectifEtab1){
		 entFanDeFleurs.addEtablissement(etabFanDeFleursBleus);
	     assertEquals(effectifEtab1, etabFanDeFleursBleus.getEffectif().getNbEffectif());
	}
    @When("Etablissement fanDeFleursBleus embauche <effEtab1Add> salarie")
    public void addOneSalarieToEtab1(@Named("effEtab1Add") int effEtab1Add) {
        if(effEtab1Add==1){
        	etabFanDeFleursBleus.getEffectif().addOneSalarie();
        }
        if(effEtab1Add == -1){
        	etabFanDeFleursBleus.getEffectif().removeOneSalarie();
        }
    }

    @Then("Le nombre d'effectifGlobal de l'entreprise fanDeFleurs passe a <effGlobal>")
    public void effectifGlobalAfterAdds(@Named("effGlobal") int effectifGlobal) {
    	assertEquals(effectifGlobal, entFanDeFleurs.getEffectifGlobal());
    }   
}
Fixture traduction de l'us

 

Etape 5

Eccrire et exécuter le test

import java.net.URL;
import java.util.List;
import java.util.Locale;

import org.jbehave.core.configuration.Configuration;
import org.jbehave.core.configuration.Keywords;
import org.jbehave.core.configuration.MostUsefulConfiguration;
import org.jbehave.core.i18n.LocalizedKeywords;
import org.jbehave.core.io.CodeLocations;
import org.jbehave.core.io.StoryFinder;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import fr.netapsys.abdou.springbootjbehave.DesignObserverJBehaveApp;
import fr.netasys.abdou.springbootjbehave.story.SpringJBehaveAbstractCfgStory;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = { DesignObserverJBehaveApp.class})
public class SpringJbehaveEntrepriseDesignObserverCfgStoryTest extends SpringJBehaveAbstractCfgStory{

	@Autowired 
	private ApplicationContext applicationCtx;
	
	protected ApplicationContext getApplicationContext(){
		return applicationCtx;
	}
	 protected URL urlCodeLocationFromClass() {
		 return CodeLocations.codeLocationFromClass(this.getClass());
	 }
	   

    protected List<String> storyPaths() {
        return new StoryFinder().findPaths(urlCodeLocationFromClass(), "**/obs*.story", "**/excluded*.story");
    }
    
    @Override
    public Configuration configuration() {
    	
    	Keywords keywords = new LocalizedKeywords(Locale.FRANCE);
        return new MostUsefulConfiguration()
                .useStoryReporterBuilder(super.storyReporterBuilder());
    }
}

 

Explications:

a) Noter que la méthode storyPath() fixe les stories concernées dans ce test via le second argument de findPaths.
Ici ce paramètre est valorisé à "**/*obs*.story" qui permet de filtrer les us avec ce pattern.
Donc seuls sont concernés les stories dont leurs noms commencent par 'obs*'.

b) Ce code ne peut être clair qu'en donnant la classe abstraite SpringJBehaveAbstractCfgStory qui se charge de configurer JBehave:

 

import static org.jbehave.core.reporters.Format.HTML;
import static org.jbehave.core.reporters.Format.TXT;

import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

import org.jbehave.core.configuration.Configuration;
import org.jbehave.core.configuration.Keywords;
import org.jbehave.core.configuration.MostUsefulConfiguration;
import org.jbehave.core.embedder.Embedder;
import org.jbehave.core.embedder.EmbedderControls;
import org.jbehave.core.i18n.LocalizedKeywords;
import org.jbehave.core.io.LoadFromClasspath;
import org.jbehave.core.io.StoryLoader;
import org.jbehave.core.io.StoryPathResolver;
import org.jbehave.core.io.UnderscoredCamelCaseResolver;
import org.jbehave.core.junit.JUnitStories;
import org.jbehave.core.reporters.FilePrintStreamFactory.ResolveToPackagedName;
import org.jbehave.core.reporters.Format;
import org.jbehave.core.reporters.StoryReporterBuilder;
import org.jbehave.core.steps.InjectableStepsFactory;
import org.jbehave.core.steps.ParameterControls;
import org.jbehave.core.steps.ParameterConverters;
import org.jbehave.core.steps.ParameterConverters.ParameterConverter;
import org.jbehave.core.steps.spring.SpringStepsFactory;
import org.springframework.context.ApplicationContext;

public abstract class SpringJBehaveAbstractCfgStory extends JUnitStories  {

    private static final String STORY_TIMEOUT_STR = "600";
    
    protected abstract ApplicationContext getApplicationContext();

    protected abstract URL urlCodeLocationFromClass();
        
    public SpringJBehaveAbstractCfgStory() {
        super();
        
        Embedder embedder = new Embedder();
        embedder.useEmbedderControls(embedderControls());
        embedder.useMetaFilters(Arrays.asList("-skip"));
        useEmbedder(embedder);
    }
    @Override
    public Configuration configuration() {	
    	Keywords keywords = new LocalizedKeywords(Locale.FRANCE);
        return new MostUsefulConfiguration()
                .useStoryPathResolver(this.storyPathResolver())
                .useStoryLoader(this.storyLoader())
                .useStoryReporterBuilder(this.storyReporterBuilder())
                .useParameterControls(this.parameterControls())
                .useParameterConverters(new ParameterConverters())     
                .useParameterConverters(
                        new ParameterConverters().addConverters(customConverters(keywords)))
                ;
    }
   
	@Override
    public InjectableStepsFactory stepsFactory() {
        return new SpringStepsFactory(configuration(), getApplicationContext());
    }

    protected StoryPathResolver storyPathResolver() {
        return new UnderscoredCamelCaseResolver();
    }

    protected StoryLoader storyLoader() {
        return new LoadFromClasspath();
    }

    protected StoryReporterBuilder storyReporterBuilder() {
        return new StoryReporterBuilder()
                .withCodeLocation(urlCodeLocationFromClass())
                .withPathResolver(new ResolveToPackagedName())
                .withFailureTrace(true)
                .withFormats(Format.CONSOLE, TXT, HTML);
    }
    
    protected ParameterControls parameterControls() {
    	return new ParameterControls()
    			.useDelimiterNamedParameters(true);
    }

    protected  ParameterConverter[] customConverters(Keywords keywords) {
        List<ParameterConverter> converters = new ArrayList<>();
        return converters.toArray(new ParameterConverter[converters.size()]);
    }
    
    protected EmbedderControls embedderControls() {
        return new EmbedderControls()
                .doIgnoreFailureInView(true)
                .useStoryTimeouts(STORY_TIMEOUT_STR);
    }
}
SpringJBehaveAbstractCfgStory

 

CONCLUSION

C'est clair que l'apport de BDD est incontestable aux yeux des acteurs de métiers puisqu'ils sont orientés métiers contrairement aux tests techniques unitaires & d'intégration. Ces derniers sont généralement écrits par les développeurs pour les développeurs (ou techniciens de développement).

Ces tests BDD peuvent être connectés à un outil de recette automatisé (squash avec OTA par exemple). Faisons ainsi, une bonne partie de la recette peut être automatisée avec des TNR ( test de non régression) ce qui permet de se concentrer sur des améliorations des services métier à offrir aux clients.

 

Le seul bémol est que la prise en main et la maintenance des tests BDD pourraient avoir un certain coût si le scope des tests n'est pas limité.

 

Si vous avez besoin d'allez plus loin, cette série (en français) peut être utile.

@Enjoy

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.