
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