Blog Arolla

Le BDD mis en oeuvre avec JBehave

Dans notre précédent article (Le BDD qu’est ce que c’est?), nous avons vu ce qu’était le BDD, son intérêt et le formalisme généralement adopté. Pas mal de théorie pour placer le contexte, tout ça c’est bien, mais ça manquait un peu de code, du graisseux !

Allez, on code !

Mise en place de notre environnement

Commençons par créer un nouveau projet Maven et ajoutons les dépendances nécessaires dans notre descripteur de projet (pom.xml):

<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>jbehave-get-started</groupId>
  <artifactId>bdd101</artifactId>
  <version>0.0.1-SNAPSHOT</version>

  <!-- ************************************************ -->
  <!-- *~~~~~~~~~~~~~~~~~PROPERTIES~~~~~~~~~~~~~~~~~~~* -->
  <!-- ************************************************ -->
  <properties>
    <maven.compiler.source>1.6</maven.compiler.source>
    <maven.compiler.target>1.6</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

    <!-- lib versions -->
    <hamcrest.version>1.2</hamcrest.version>
    <spring.version>3.1.1.RELEASE</spring.version>
    <slf4j.version>1.6.4</slf4j.version>
    <jbehave.version>3.6.6</jbehave.version>
  </properties>

  <!-- ************************************************ -->
  <!-- *~~~~~~~~~~~~~~~~DEPENDENCIES~~~~~~~~~~~~~~~~~~* -->
  <!-- ************************************************ -->
  <dependencies>
    <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
    <!-- ~~~~~~~~~~~~~Commons~~~~~~~~~~~~~~~ -->
    <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.1</version>
    </dependency>

    <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
    <!-- ~~~~~~~~~~~~~~Spring~~~~~~~~~~~~~~~ -->
    <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
    </dependency>

    <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
    <!-- ~~~~~~~~~~~~~~~~Log~~~~~~~~~~~~~~~~ -->
    <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>log4j-over-slf4j</artifactId>
      <version>${slf4j.version}</version>
    </dependency>

    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.0.0</version>
    </dependency>

    <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
    <!-- ~~~~~~~~~~~~~JBehave~~~~~~~~~~~~~~~ -->
    <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
    <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>de.codecentric</groupId>
      <artifactId>jbehave-junit-runner</artifactId>
      <version>1.0.1-SNAPSHOT</version>
    </dependency>

    <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
    <!-- ~~~~~~~~~~~~~~~Test~~~~~~~~~~~~~~~~ -->
    <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit-dep</artifactId>
    </dependency>
    <dependency>
      <groupId>org.hamcrest</groupId>
      <artifactId>hamcrest-library</artifactId>
    </dependency>
    <dependency>
      <groupId>org.hamcrest</groupId>
      <artifactId>hamcrest-core</artifactId>
    </dependency>
  </dependencies>

  <!-- ************************************************ -->
  <!-- *~~~~~~~~~~~DEPENDENCY MANAGEMENT~~~~~~~~~~~~~~* -->
  <!-- ************************************************ -->
  <dependencyManagement>
    <dependencies>
      <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
      <!-- ~~~~~~~~~~~~~~Spring~~~~~~~~~~~~~~~ -->
      <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
      <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
      </dependency>

      <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
      <!-- ~~~~~~~~~~~~~~~Test~~~~~~~~~~~~~~~~ -->
      <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
      <dependency>
        <groupId>junit</groupId>
        <artifactId>junit-dep</artifactId>
        <version>4.10</version>
      </dependency>
      <dependency>
        <groupId>org.hamcrest</groupId>
        <artifactId>hamcrest-library</artifactId>
        <version>${hamcrest.version}</version>
      </dependency>
      <dependency>
        <groupId>org.hamcrest</groupId>
        <artifactId>hamcrest-core</artifactId>
        <version>${hamcrest.version}</version>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <!-- ************************************************ -->
  <!-- *~~~~~~~~~~~~~~~~~REPOSITORIES~~~~~~~~~~~~~~~~~* -->
  <!-- ************************************************ -->
  <repositories>
    <repository>
      <id>codehaus-releases</id>
      <name>Codehaus Nexus Repository Manager</name>
      <url>https://nexus.codehaus.org/content/repositories/releases/</url>
    </repository>
    <repository>
      <id>sonatype-snapshots</id>
      <name>Sonatype Snapshots</name>
      <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
    </repository>
  </repositories>

</project>

On notera les dépendances à JBehave, quelques utilitaires pour simplifier l’écriture de nos tests,et Spring pour l’injection de dépendance.

On retiendra aussi la dépendance à jbehave-junit-runner qui permet une intégration encore plus riche avec Junit en utilisant un lanceur spécial: de.codecentric.jbehave.junit.monitoring.JUnitReportingRunner. Ce lanceur permet de visualiser chaque étape de chaque scénario comme un test spécifique, il est ainsi beaucoup plus facile d’identifier à quelle étape notre scénario a échoué. De plus, cela s’intègre parfaitement avec la vue Eclipse, JUnit permettant un retour immédiat lorsque les tests sont exécutés directement depuis l’IDE. La page du projet correspondant peut être trouvée ici: Code Centric ~ jbehave-junit-runner

Comme nous nous baserons uniquement sur les annotations Spring pour l’injection de dépendances et la définition de nos étapes, nous nous passerons de fichier de configuration Spring. Le contexte sera directement initialisé par la méthode suivante:

public static AnnotationConfigApplicationContext createContextFromBasePackages(String... basePackages) {
	AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
	applicationContext.scan(basePackages);
	applicationContext.refresh();
	return applicationContext;
}

Voila pour l’infrastructure, il nous reste à définir la classe qui lancera nos scénarios. Nous nous baserons pour cela sur le framework JUnit pour lequel jbehave fournit les adaptateurs nécessaires.

Au risque de faire un peu peur au début, nous opterons tout de suite pour une description assez riche de notre environnement de tests. JBehave fournit de multiples façons diverses et variées pour configurer l’environnement d’exécution des scénarios, nous choisissons ici la moins « magique » mais la plus verbeuse et surtout celle qui permet un contrôle total de chaque composant.

...

import bdd101.util.Springs;
import bdd101.util.UTF8StoryLoader;
import de.codecentric.jbehave.junit.monitoring.JUnitReportingRunner;

@RunWith(JUnitReportingRunner.class)
public class AllStoriesTest extends JUnitStories {

    private final CrossReference xref = new CrossReference();

    public AllStoriesTest() {
        configuredEmbedder()//
                .embedderControls()//
                .doGenerateViewAfterStories(true)//
                .doIgnoreFailureInStories(false)//
                .doIgnoreFailureInView(true)//
                .doVerboseFailures(true)//
                .useThreads(2)//
                .useStoryTimeoutInSecs(60);
    }

    @Override
    public Configuration configuration() {
        Class<? extends Embeddable> embeddableClass = this.getClass();
        URL codeLocation = codeLocationFromClass(embeddableClass);
        StoryReporterBuilder storyReporter = //
        new StoryReporterBuilder() //
                .withCodeLocation(codeLocation) //
                .withDefaultFormats() //
                .withFormats(CONSOLE, //
                        HTML_TEMPLATE) //
                .withFailureTrace(true) //
                .withFailureTraceCompression(true) //
                .withCrossReference(xref)
                ;
        return new MostUsefulConfiguration() //
                .useStoryLoader(new UTF8StoryLoader(embeddableClass)) //
                .useStoryReporterBuilder(storyReporter) //
                .useStepMonitor(xref.getStepMonitor())//
                ;
    }

    @Override
    protected List<String> storyPaths() {
        URL searchInURL = codeLocationFromClass(this.getClass());
        return new StoryFinder().findPaths(searchInURL, "**/*.story", "");
    }

    @Override
    public InjectableStepsFactory stepsFactory() {
        return new SpringStepsFactory(configuration(),
                Springs.createAnnotatedContextFromBasePackages("bdd101"));
    }
}

Quelques explications:

  • L’annotation @RunWith(JUnitReportingRunner.class) indique à JUnit le lanceur qui doit être utilisé pour exécuter notre test.
  • Le nom de notre classe finit par Test afin de suivre les conventions usuelles et étend la classe JBehave: JUnitStories afin de faciliter l’intégration JUnit/JBehave.
  • Notre constructeur définit l’Embedder JBehave (c’est à dire l’environnement global d’exécution des tests JBehave) qui sera utilisé. Nous verrons les options activées au fur et à mesure de notre article. Ce qu’il faut retenir, c’est que ces paramètres permettent de contrôler l’exécution des tests (useStoryTimeoutInSecs, useThreads) et la perception globale des tests (doVerboseFailures) : un test en échec arrête-t-il l’exécution (doIgnoreFailureInStories) ou est-ce lors de la génération du rapport consolidé (doGenerateViewAfterStories) que l’on considérera que l’exécution est en échec (doIgnoreFailureInView) ?
    Chaque test JBehave étant lancé de manière indépendante, à la fin de chaque test, JBehave consolide les résultats dans un unique rapport.
  • Vient ensuite la seconde partie de la configuration de notre environnement d’exécution. On retiendra pour le moment deux paramètres importants:
    • les types de rapport qui seront générés, avec notamment la sortie CONSOLE qui facilitera la phase de développement dans notre IDE, et la sortie HTML_TEMPLATE que nous verrons plus tard et qui permet d’avoir un joli rapport html.
    • L’utilisation d’une classe spéciale UTF8StoryLoader qui nous permettra de nous affranchir des problématiques d’encodage qui peuvent apparaître dans le cas de développement multi-plateforme. On impose ici l’utilisation systématique de l’UTF8, ce qui correspond au choix que nous avons fait dans notre fichier pom.xml de maven.
  • On trouve ensuite la méthode permettant de récupérer la liste des fichiers *.story à exécuter. Il y (au moins) deux pièges dans cette déclaration:
    • Le premier (qui est aussi directement lié à l’utilisation de notre classe UTF8StoryLoader) est que les fichiers seront chargés comme ressources Java, il convient donc d’indiquer des chemins relatifs à notre classpath. Ce qui nous amène au second piège:
    • La méthode utilisée ici se base sur l’emplacement de notre classe de test, il est donc important de placer nos fichiers *.story dans les ressources maven correspondantes: src/test/resources (copiées dans target/test-classes) si notre lanceur est dans un package de src/test/java, ou src/main/resources (copiées dans target/classes) si notre lanceur est dans un package de src/main/java.

Notre premier scénario

Commençons simplement par le développement d’une petite calculatrice.

Écrivons notre premier scénario src/test/resources/stories/calculator.story:

Scenario: 2+2

Given a variable x with value 2
When I add 2 to x
Then x should equal to 4

Editeur Eclipse de scenario

Astuce du plugin JBehave On peux constater que toutes nos étapes sont soulignées en rouge pour indiquer que notre éditeur n’est pas parvenu à les associer au code java correspondant.

Exécutons notre lanceur de scénario: Run as / JUnit Test sur la classe AllStoriesTest.

Vue JUnit Eclipse

La console Eclipse (Rappel: la sortie console est activée grâce à l’option CONSOLE) affiche alors la sortie suivante:

(stories/calculator.story)
Scenario: 2+2
Given a variable x with value 2 (PENDING)
When I add 2 to x (PENDING)
Then x should equal to 4 (PENDING)

@Given("a variable x with value 2")
@Pending
public void givenAVariableXWithValue2() {
  // PENDING
}

@When("I add 2 to x")
@Pending
public void whenIAdd2ToX() {
  // PENDING
}

@Then("x should equal to 4")
@Pending
public void thenXShouldEqual4() {
  // PENDING
}

Faisons un petit point du résultat obtenu et qui peut être confus au premier abord:

  • Notre test JUnit est vert ! Ce qui est déroutant !
  • Grâce au lanceur JUnit
    (de.codecentric.jbehave.junit.monitoring.JUnitReportingRunner) la vue JUnit nous affiche l’intégralité des étapes qui ont été jouées: BeforeStories, notre scénario et les étapes AfterStories.
  • Toutes les étapes de notre scénario sont marquées PENDING et ont été ignorées lors de l’exécution du test.
  • PENDING signifie que les étapes présentes dans notre fichier story n’ont pas leurs correspondants dans le code Java, où les méthodes qui doivent être invoquées sont annotées avec le texte du step correspondant. C’est ce qui est d’ailleurs proposé par JBehave en suggestion d’implémentation dans la console. Pour mettre en échec les étapes PENDING et donc pour que notre test ne soit plus vert, il suffit de changer la stratégie par défaut dans la classe AllStoriesTest par FailingUponPendingStep:
  ...
  return new MostUsefulConfiguration() //
                .useStoryLoader(new UTF8StoryLoader(embeddableClass)) //
                .useStoryReporterBuilder(storyReporter) //
                .usePendingStepStrategy(new FailingUponPendingStep())
                .useStepMonitor(xref.getStepMonitor())//
                ;

Créons donc une classe bdd101.calculator.CalculatorSteps qui contiendra nos premières définitions d’étapes (Steps) basées en partie sur les propositions faites par JBehave dans la console:

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 bdd101.util.StepsDefinition;

@StepsDefinition
public class CalculatorSteps {

    @Given("a variable $variable with value $value")
    public void defineNamedVariableWithValue(String variable, int value) {
        throw new UnsupportedOperationException();
    }

    @When("I add $value to $variable")
    public void addValueToVariable(@Named("variable") String variable, 
                                   @Named("value")int value) {
        throw new UnsupportedOperationException();
    }

    @Then("$variable should equal to $expected")
    public void assertVariableEqualTo(String variable, int expectedValue) {
        throw new UnsupportedOperationException();
    }
}

Relisons cette classe ligne par ligne:

  • @StepsDefinition est une annotation personnelle qui permet à la fois de marquer cette classe comme contenant des définitions d’étapes (ce qui est purement informatif) et qui permet à Spring de la détecter au moment où il va parcourir les classes pour la construction de son contexte; pour plus d’informations, voir la documentation de Spring sur l’utilisation des annotations (Spring – Using filters to customize scanning).
import java.lang.annotation.Documented;
import org.springframework.stereotype.Component;

@Documented
@Component
public @interface StepsDefinition {}
  • Les étapes sont définies grâce à des annotations spécifiques: @Given, @When et @Then.
  • La valeur de chaque annotation correspond à la phrase dans le scénario. Nous avons gardé la configuration par défaut qui spécifie que dans ces phrases, les mots commençant par désignent les variables. Ainsi, la première annotation permet de supporter les phrases suivantes:
    • Given a variable x with value 2
    • Given a variable y with value 17
  • Les variables sont passées en paramètre dans le même ordre qu’elles apparaissent dans la phrase. Si cet ordre n’est pas satisfaisant, il est possible d’annoter chaque paramètre, @Named, pour indiquer la variable qu’il référence (Lignes 17 et 18).
  • La conversion d’une variable dans le type du paramètre se fait automatiquement à l’aide des converteurs prédéfinis. Il est possible d’ajouter de nouveaux converteurs.
  • Toutes nos étapes génèrent une exception dans notre implémentation initiale.

Editeur Eclipse de scenario

Astuce du plugin JBehave On peux constater qu’une fois ces étapes enregistrées, notre éditeur de scénario nous indique que toutes nos étapes sont bien définies. Les variables apparaissent avec une couleur différente mettant en évidence leurs emplacements.

Exécutons à nouveau notre test:

Vue JUnit Eclipse

(stories/calculator.story)
Scenario: 2+2
Given a variable x with value 2 (FAILED)
(java.lang.UnsupportedOperationException)
When I add 2 to x (NOT PERFORMED)
Then x should equal to 4 (NOT PERFORMED)

java.lang.UnsupportedOperationException
    at bdd101.calculator.CalculatorSteps.defineNamedVariableWithValue(CalculatorSteps.java:14)
    (reflection-invoke)

On constate désormais que notre test est en échec, que seule la première étape à été exécutée mais qu’elle a échoué FAILED en générant une exception, ce qui correspond bien à notre implémentation. La suite du scénario n’a pas été exécutée: NOT PERFORMED

Cycle BDD et Cycles TDD

Passons rapidement sur le développement de notre calculatrice (par une approche de type TDD par exemple) pour arriver à une implémentation fonctionnelle (au sens « qui fonctionne »…). Nous obtenons alors la classe Calculator suivante:

import java.util.HashMap;
import java.util.Map;

public class Calculator {
    private final Map<String, Integer> context;

    public Calculator () {
      context = new HashMap<String, Integer>();
    }

    public void defineVariable(String variable, int value) {
        context.put(variable, value);
    }
    
    public void addToVariable(String variable, int value) {
        int existing = getVariableValueOrFail(variable);
        context.put(variable, value + existing);
    }
    
    public int getVariableValue(String variable) {
        return getVariableValueOrFail(variable);
    }

    protected int getVariableValueOrFail(String variable) {
        Integer existing = context.get(variable);
        if(existing==null)
            throw new IllegalStateException(
              "Variable <" + variable + "> is not defined");
        return existing;
    }
}

Il est désormais nécessaire de faire le lien entre notre calculateur (Calculator) et la définition de nos étapes (CalculatorSteps).

Astuce du plugin JBehave Dans notre éditeur de scénario, il est possible d’accéder directement à la méthode correspondante soit par Ctrl+Clic sur l’étape concernée soit en ayant le curseur sur la ligne correspondante et en appuyant sur Ctrl+G (GO!).

public class CalculatorSteps {
    private Calculator calculator = new Calculator ();

    @Given("a variable $variable with value $value")
    public void defineNamedVariableWithValue(String variable, int value) {
        calculator.defineVariable(variable, value);
    }

    ...
}

En relançant notre test, nous obtenons cette fois:

Vue JUnit Eclipse

Bon ! A ce stade, vous devriez avoir un bon aperçu du fonctionnement, faisons un petit saut dans le temps pour arriver à l’implémentation finale de nos étapes:

...

@When("I add $value to $variable")
public void addValueToVariable(@Named("variable") String variable, 
                               @Named("value")int value) {
    calculator.addToVariable(variable, value);
}

@Then("$variable should equal to $expected")
public void assertVariableEqualTo(String variable, int expectedValue) {
    assertThat(calculator.getVariableValue(variable), equalTo(expectedValue));
}

Avant de faire un petit point avec notre client, enrichissons un peu notre histoire en lui ajoutant de nouveaux scénarios.

On commencera par un petit copier/coller (et oui, on a le droit !) pour vérifier que l’on peut utiliser d’autres noms de variables et d’autres valeurs que 2. Et même que l’on peut mixer l’utilisation de plusieurs variables.

Scenario: 2+2 avec une variable y

Given a variable y with value 2
When I add 2 to y
Then y should equal to 4

Scenario: 37+5 avec une variable UnBienJoli_Nom

Given a variable UnBienJoli_Nom with value 37
When I add 5 to UnBienJoli_Nom
Then UnBienJoli_Nom should equal to 42

Scenario: 7+2 et 9+4 avec une variable y et une variable x

Given a variable y with value 7
Given a variable x with value 9
When I add 2 to y
When I add 4 to x
Then x should equal to 13
Then y should equal to 9

Hummm et si on utilise une variable qui n’existe pas ? Eh bien la réponse est à voir avec le client! Faisons un petit point avec notre client. Il nous dit que ça serait bien si on pouvait faire plusieurs additions sur la même variable.

Scenario: 37+5+6+17 

Given a variable x with value 37
When I add 5 to y
And I add 6 to x
And I add 17 to x
Then x should equal to 65

Astuce du plugin JBehave Dans notre éditeur de scénario, il est possible d’obtenir une complétion automatique parmi les étapes disponibles par Ctrl+Espace. Il est aussi possible de faire une recherche parmi toutes les étapes disponibles en pressant Ctrl+J

Plugin JBehave recherche rapide

Plugin JBehave recherche rapide avec filtre

Relançons notre test:

Scenario: 37+5+6+17
Given a variable x with value 37
When I add 5 to y
And I add 6 to x
And I add 17 to x
Then x should equal to 65 (FAILED)
(java.lang.AssertionError: 
Expected: <65>
     got: <60>
)

Humpf ! ça, c’était pas prévu ! Que s’est-il passé ? En regardant de plus près, on peut voir que l’on s’est trompé ligne 4, on ajoute 5 à la variable y au lieu de la variable x
Ce qui soulève deux problèmes: comment se fait-il que la variable y existe et pourquoi n’a-t-on pas eu d’erreur ? Ce qui nous permet au passage de voir avec notre client comment il souhaite prendre en compte l’utilisation de variable non définie. Ensemble, nous définissons alors un nouveau scénario:

Scenario: Undefined variable displays error message

When I add 5 to y
Then the calculator should display the message 'Variable <y> is not defined'

Maintenant, intéressons-nous à notre erreur précédente: comment se fait-il que nous n’ayons pas eu d’erreur (IllegalStateException)? Et bien, tout simplement parce que l’un des scénarios précédent a défini cette variable, et que les classes définissant les étapes ne sont pas réinstanciées à chaque test: nous utilisons donc la même instance de Calculator pour tous les scénarios.

Les classes définissant les étapes ne sont instanciées qu’une seule fois pour tous les fichiers *.story et pour tous les scénarios d’un fichier story. Et même de manière concurrente si l’on spécifie que les scénarios peuvent être exécutés à travers plusieurs Thread.

Cela nous amène à présenter quelques bonnes pratiques:

  • ne pas stocker d’états dans les classes définissant les étapes
  • utiliser les annotations @BeforeStories, @BeforeStory, @BeforeScenario pour réinitialiser les états entre chaque scénario. Dans le cas de tests unitaires, on pourra se contenter de réinitialiser uniquement le contexte du test avant chaque scénario @BeforeScenario. Tandis que dans le cas des tests d’intégration, on pourra par exemple démarrer le serveur Sélenium, ou le serveur d’application, au tout début des tests dans une méthode annotée @BeforeStories, réinitialiser la base de données avant chaque histoire @BeforeStory et réinitialiser le contexte du test avant chaque scénario @BeforeScenario.
  • utiliser les annotations @AfterStories, @AfterStory et @AfterScenario pour fermer et nettoyer les ressources correspondantes.

Afin de garder une infrastructure de test simple qui nous permettra de travailler en environnement concurrent, nous opterons pour l’utilisation de variable ThreadLocal pour maintenir l’état de chaque scénario. Ainsi, deux scénarios s’exécutant en parallèle (chacun dans leur thread) disposeront chacun de leur propre contexte.

public class CalculatorContext {

    private static ThreadLocal<CalculatorContext> threadContext = 
            new ThreadLocal<CalculatorContext>();
    
    public static CalculatorContext context() {
        return threadContext.get();
    }
    
    public static Calculator calculator() {
        return context().getCalculator();
    }
    
    public static void initialize() {
        // one does not rely on ThreadLocal#initialValue()
        // so that one is sure only initialize create a new
        // instance
        threadContext.set(new CalculatorContext());
    }
    public static void dispose () {
        threadContext.remove();
    }
    
    private final Calculator calculator;
    private Exception lastError;
    
    public CalculatorContext() {
        calculator = new Calculator();
    }
    
    public Calculator getCalculator() {
        return calculator;
    }
    
    public void setLastError(Exception lastError) {
        this.lastError = lastError;
    }
    
    public Exception getLastError() {
        return lastError;
    }
}

Modifions enfin notre classe CalculatorSteps:

package bdd101.calculator;

import static bdd101.calculator.CalculatorContext.calculator;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;

import org.jbehave.core.annotations.AfterScenario;
import org.jbehave.core.annotations.BeforeScenario;
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 bdd101.util.StepsDefinition;

@StepsDefinition
public class CalculatorSteps {
    
    @BeforeScenario
    public void inializeScenario() {
        CalculatorContext.initialize();
    }

    @AfterScenario
    public void disposeScenario() {
        CalculatorContext.dispose();
    }
    
    @Given("a variable $variable with value $value")
    public void defineNamedVariableWithValue(String variable, int value) {
        calculator().defineVariable(variable, value);
    }

    @When("I add $value to $variable")
    public void addValueToVariable(@Named("variable") String variable, 
                                   @Named("value")int value) {
        calculator().addToVariable(variable, value);
    }
}

Relançons les tests, et cette fois nous obtenons bien l’exception souhaitée:

...

Scenario: Undefined variable displays error message
When I add 5 to y (FAILED)
(java.lang.IllegalStateException: Variable <y> is not defined)
Then the calculator should display the message 'Variable y is not defined' (PENDING)
@Then("the calculator should display the message 'Variable y is not defined'")
@Pending
public void thenTheCalculatorShouldDisplayTheMessageVariableYIsNotDefined() {
  // PENDING
}

Modifions légèrement notre classe de définitions d’étapes pour gérer l’exception:

...
    @When("I add $value to $variable")
    public void addValueToVariable(@Named("variable") String variable, 
                                   @Named("value")int value) {
        try {
            calculator().addToVariable(variable, value);
        } catch (Exception e) {
            context().setLastError(e);
        }
    }
...

L’erreur pouvant être de nature « métier » (le code est ici simplifié), ce n’est généralement pas à une étape de type Given ou When de la traiter. Les assertions devraient autant que possible se situer dans les méthodes Then.

Puis enfin, ajoutons l’étape de vérification:

...

@Then("the calculator should display the message '$errorMessage'")
public void assertErrorMessageIsDisplayed(String errorMessage) {
  Exception lastError = context().getLastError();
  assertThat("Not in error situtation", lastError, notNullValue());
  assertThat("Wrong error message", lastError.getMessage(), equalTo(errorMessage));
}

Afin de s’assurer que tous nos scénarios précédents restent cohérents, nous ajoutons aussi l’étape suivante the calculator should not be in error à la fin de chaque scénario.

Scenario: 2+2

Given a variable x with value 2
When I add 2 to x
Then x should equal to 4
And the calculator should not be in error

Scenario: 2+2 avec une variable y

Given a variable y with value 2
When I add 2 to y
Then y should equal to 4
And the calculator should not be in error

...

La méthode correspondante à l’étape:

...
@Then("the calculator should not be in error")
public void assertNoErrorMessageIsDisplayed() {
  Exception lastError = context().getLastError();
  assertThat(lastError, nullValue());
}

Conclusion

Un schéma vaut mieux qu’un long discours:

There's an app for that!

En attendant le « Specs Creator », le BDD est une bonne alternative !

Références et Liens

Le code complet est disponible ici: jbehave-get-started.

Articles:

Outils:

6 comments for “Le BDD mis en oeuvre avec JBehave

  1. 22 juin 2012 at 10 h 08 min

    Great post! Thanks for providing this walk-through including Maven and Eclipse tools. Interestingly, it works very well even when translated to English with Google.

    BTW: you can skip the snapshot dependency for the jbehave-junit-runner 😉

  2. Stephane
    22 juin 2012 at 19 h 08 min

    Bonjour Arnauld,

    le plugin Jbehave Eclipse ne fonctionne avec mon environnement
    Windows Vista
    Eclipse Helios
    jdk160_14_R27.6.5-32

    J’obtiens le message suivant à l’ouverture d’eclipse (.metadata/.log)

    !ENTRY org.eclipse.ui.workbench 4 2 2012-06-22 19:01:00.912
    !MESSAGE Problems occurred when invoking code from plug-in: « org.eclipse.ui.workbench ».
    !STACK 0
    java.lang.NoSuchFieldError: INSTANCE
    at org.technbolts.eclipse.preferences.PreferencesHelper.getHelper(PreferencesHelper.java:5)
    at org.technbolts.jbehave.eclipse.preferences.ProjectPreferences.(ProjectPreferences.java:48)
    at org.technbolts.jbehave.eclipse.JBehaveProject.initializeProjectPreferencesAndListener(JBehaveProject.java:89)
    at org.technbolts.jbehave.eclipse.JBehaveProject.(JBehaveProject.java:71)
    at org.technbolts.jbehave.eclipse.JBehaveProjectRegistry.getOrCreateProject(JBehaveProjectRegistry.java:79)
    at org.technbolts.jbehave.eclipse.editors.story.StoryEditor.getJBehaveProject(StoryEditor.java:285)
    at org.technbolts.jbehave.eclipse.editors.story.StoryEditor.dispose(StoryEditor.java:134)
    at org.eclipse.ui.internal.EditorReference.createPartHelper(EditorReference.java:705)
    at org.eclipse.ui.internal.EditorReference.createPart(EditorReference.java:465)
    at org.eclipse.ui.internal.WorkbenchPartReference.getPart(WorkbenchPartReference.java:595)
    at org.eclipse.ui.internal.PartPane.setVisible(PartPane.java:313)
    at org.eclipse.ui.internal.presentations.PresentablePart.setVisible(PresentablePart.java:180)
    at org.eclipse.ui.internal.presentations.util.PresentablePartFolder.select(PresentablePartFolder.java:270)
    at org.eclipse.ui.internal.presentations.util.LeftToRightTabOrder.select(LeftToRightTabOrder.java:65)
    at org.eclipse.ui.internal.presentations.util.TabbedStackPresentation.selectPart(TabbedStackPresentation.java:473)
    at org.eclipse.ui.internal.PartStack.refreshPresentationSelection(PartStack.java:1254)
    at org.eclipse.ui.internal.PartStack.handleDeferredEvents(PartStack.java:1222)
    at org.eclipse.ui.internal.LayoutPart.deferUpdates(LayoutPart.java:400)
    at org.eclipse.ui.internal.PartSashContainer.handleDeferredEvents(PartSashContainer.java:1409)
    at org.eclipse.ui.internal.LayoutPart.deferUpdates(LayoutPart.java:400)
    at org.eclipse.ui.internal.WorkbenchPage.handleDeferredEvents(WorkbenchPage.java:1420)
    at org.eclipse.ui.internal.WorkbenchPage.deferUpdates(WorkbenchPage.java:1410)
    at org.eclipse.ui.internal.WorkbenchPage.access$14(WorkbenchPage.java:1401)
    at org.eclipse.ui.internal.WorkbenchPage$16.runWithException(WorkbenchPage.java:3304)
    at org.eclipse.ui.internal.StartupThreading$StartupRunnable.run(StartupThreading.java:31)
    at org.eclipse.swt.widgets.RunnableLock.run(RunnableLock.java:35)
    at org.eclipse.swt.widgets.Synchronizer.runAsyncMessages(Synchronizer.java:134)
    at org.eclipse.swt.widgets.Display.runAsyncMessages(Display.java:4041)
    at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:3660)
    at org.eclipse.ui.application.WorkbenchAdvisor.openWindows(WorkbenchAdvisor.java:803)
    at org.eclipse.ui.internal.Workbench$31.runWithException(Workbench.java:1566)
    at org.eclipse.ui.internal.StartupThreading$StartupRunnable.run(StartupThreading.java:31)
    at org.eclipse.swt.widgets.RunnableLock.run(RunnableLock.java:35)
    at org.eclipse.swt.widgets.Synchronizer.runAsyncMessages(Synchronizer.java:134)
    at org.eclipse.swt.widgets.Display.runAsyncMessages(Display.java:4041)
    at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:3660)
    at org.eclipse.ui.internal.Workbench.runUI(Workbench.java:2537)
    at org.eclipse.ui.internal.Workbench.access$4(Workbench.java:2427)
    at org.eclipse.ui.internal.Workbench$7.run(Workbench.java:670)
    at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:332)
    at org.eclipse.ui.internal.Workbench.createAndRunWorkbench(Workbench.java:663)
    at org.eclipse.ui.PlatformUI.createAndRunWorkbench(PlatformUI.java:149)
    at org.eclipse.ui.internal.ide.application.IDEApplication.start(IDEApplication.java:115)
    at org.eclipse.equinox.internal.app.EclipseAppHandle.run(EclipseAppHandle.java:196)
    at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:110)
    at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:79)
    at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:369)
    at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:179)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:619)
    at org.eclipse.equinox.launcher.Main.basicRun(Main.java:574)
    at org.eclipse.equinox.launcher.Main.run(Main.java:1407)

    As tu déjà rencontré le problème ?

    @+

  3. Arnauld
    25 juin 2012 at 15 h 25 min

    Thank you for your comment, i’ll remove the SNAPSHOT 😉

  4. Arnauld
    25 juin 2012 at 15 h 29 min

    Bonjour Stéphane,
    Il s’agit malheureusement d’un problème connu sur Helios. (Issue 46 et Commentaire dans la 28). Le mode de fonctionnement des préférences a changé entre Helios et Indigo, et le plugin utilise le mécanisme de Indigo.
    Je te recommanderai donc d’utiliser la dernière version de Eclipse Indigo ou +.

  5. Kissenlall Rambojun
    28 septembre 2012 at 8 h 40 min

    Bonjour Arnaud,

    Est-ce que le plugin JBEBAVE est compatible avec springsource tool suite (STS) ?

    Merci,
    Kissen

  6. Arnauld
    1 octobre 2012 at 10 h 43 min

    Coucou,
    Le plugin est compatible avec STS (STS est basé sur Eclipse). Il faut juste s’assurer que la version d’Eclipse est bien Indigo, ce qui est le cas avec les dernières versions de STS.

    Par ailleurs, le plugin fait désormais parti du projet JBehave et dispose d’une url d’update pour l’installer/le mettre à jour, plus de détail ici
    http://jbehave.org/eclipse-integration.html

    Bonne journée,
    Arnauld

Laisser un commentaire

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

* Copy This Password *

* Type Or Paste Password Here *