home : Développement : Tests (recette, mocks, ...) :
Intégrer JBehave 3.x aux autres frameworks de tests
Jeudi, 24 Mars 2011 17:09 PDF Imprimer Envoyer

Depuis sa version 3.0 (releasée en août 2010), JBehave peut être intégré à une classe de tests sans passer par un système d'héritage, comme c'était le cas avec les versions précédentes. Cela devenait particulièrement handicapant lorsque l'on souhaitait le coupler avec un autre framework de test type Unitils ou DbUnit, eux-mêmes basés sur ce principe. Désormais, plusieurs solutions sont possibles : injection par annotations, instanciation manuelle et... héritage. Nous allons voir dans cet article comment coupler nos deux frameworks de tests favoris.

Cet article n'a pas pour objectif de présenter le concept de BDD ou l'outil JBehave lui-même.

Avant de commencer, il faut préciser que la documentation présente sur le site reste un gros point faible de l'outil : entre concepts techniques survolés, exemples Java non compilables, classes inexistantes, il est très difficile de démarrer avec JBehave. Cela est en tout cas vrai pour la dernière version stable de JBehave, la 3.1.2 au moment où j'écris ces lignes. Cet article a été donc écrit de façon à vous aider à passer outre ces lacunes.

Intégration en tant que tests "JUnit-like"

L'approche proposée par défaut par JBehave est d'exécuter les tests fonctionnels à la phase de tests d'intégration de Maven. Pour cela les tests doivent être implémenter l'interface Embeddable pour être automatiquement reconnus. Cependant, malgré toutes mes tentatives et bien que les étapes semblaient s'exécuter, je n'ai jamais réussi à intégrer les fonctionnalités d'Unitils.

J'ai donc fini par abandonner cette approche, pour en utiliser une autre (également proposée par l'outil) : rendre les tests JBehave exécutables en tant que tests JUnit. Cette approche a plusieurs avantages :

  • Les fonctionnalités de Unitils (injection Spring, module DbUnit, ...) peuvent être utilisées,
  • Les tests sont automatiquement reconnus par l'IDE (Eclipse pour ma part) et peuvent donc y être exécutés comme tous les autres tests unitaires. Libre à vous ensuite de les exclure dans votre profil Maven par défaut, pour les exécuter sous un autre profil.

Tout le reste de cet article se base donc sur ce principe.

Principe de l'Embedder

J'ai personnellement mis du temps à comprendre les rôles que jouaient les différentes classes de base de JBehave (JUnitScenario, JUnitStory, JUnitStories, Embedder, Embeddable, ...). Il s'avère qu'elles sont chacune une représentation différente des différentes approches proposées. Dans notre cas, seuls les éléments Embeddable et Embedder nous intéresserons.

ll faut voir l'Embedder comme le point d'entrée des fonctionnalités techniques de JBehave. Il s'agit de la factory qui va permettre le chargement, l'exécution puis le reporting des différents scenarii ou stories. L'interface Embeddable permet de signaler qu'une classe utilisera un embedder.

Ce dernier peut lui même être exécuté ou accédé de différentes façons : via l'héritage d'une classe de base (comme JUnitStory), par injection via annotations ou via une instanciation manuelle. Afin de conserver la compatibilité avec les différents outils souhaités, nous allons le gérer manuellement.

Création de la classe mère

Commençons par créer la classe mère de tous nos tests fonctionnels, qui nous permet de lier les outils JBehave et Unitils :

 
public abstract class AbstractJBehaveTestCase extends UnitilsJUnit4 implements Embeddable {
 
   private L ist candidateSteps = new A rrayList();
   private Embedder embedder = new Embedder();
 
   protected void addSteps(O bject... candidateSteps) {
      this.candidateSteps.addAll(
         new InstanceStepsFactory(
            embedder.configuration(), candidateSteps).createCandidateSteps());
   }
 
   private L ist storyPaths() {
 
      StoryPathResolver resolver =
         new UnderscoredCamelCaseResolver(".story").removeFromClassName("TestCase");
      S tring storyPath = resolver.resolve(this.getClass());
 
      return asList(storyPath);
   }
 
   public void useEmbedder(Embedder embedder) {
      // just to implement Embeddable, required to use UnderscoredCamelCaseResolver
   }
 
   @Test
   public void run() throws T hrowable {
 
      embedder.configuration().usePendingStepStrategy(new FailingUponPendingStep());
 
      embedder.embedderControls()
         .doGenerateViewAfterStories(true)
         .doIgnoreFailureInStories(true)
         .doIgnoreFailureInView(true);
 
      embedder.useCandidateSteps(candidateSteps);
      embedder.runStoriesAsPaths(storyPaths());
   }
}
 

Notre classe AbstractJBehaveTestCase hérite elle-même de UnitilsJUnit4, ce qui lui permet de bénéficier de toutes ses fonctionnalités, comme l'injection des beans Spring via l'annotation @SpringBean, le remplissage ou la vérification de la base de données via le module DbUnit grâce aux annotations @DataSet et @ExpectedDataSet... Notez que si votre projet inclut une classe mère de tous les tests, vous pouvez très bien en hériter pour adapter votre hiérarchie de classes.

Elle implémente également l'interface Embeddable, pour une raison assez idiote : si l'on souhaite utiliser le UnderscoredCamelCaseResolver présent dans la méthode storyPaths, il faut que notre classe l'implémente, bien que le resolver n'en aie aucun besoin. Cette implémentation n'a pas d'autre utilité. Nous nous retrouvons donc avec la méthode useEmbedder, totalement inutile, et la méthode run que nous réutilisons pour lancer le test via l'annotation JUnit @Test.

La méthode run est également le siège de la configuration de l'embedder. A vous de choisir si vous souhaitez l'externaliser dans une méthode utilitaire tierce, voire l'injecter via Spring. Les deux appels clés de cette méthode sont :

  • useCandidateSteps, qui permet l'utilisation des étapes correspondantes au scénario qui sera exécuté,
  • runStoriesAsPaths, qui permet l'exécution du (ou des) scenario à exécuter.

La méthode addCandidateSteps est une méthode utilitaire permettant l'ajout, une fois l'initialisation complète de la classe, des steps JBehave.

Enfin, la méthode storyPaths retourne la liste des scenarii à exécuter sous forme de chemins relatifs au répertoire target de Maven. Dans notre exemple, nous avons pris le parti de considérer qu'une classe de tests correspondait à un seul scenario, afin de clarifier les développements et surtout de pouvoir moduler les contraintes sur le test. Vous pouvez cependant choisir d'exécuter l'ensemble des scenarii à partir de cette classe unique, en retournant la liste complèt souhaité (vous pouvez utiliser des wildcards). Le nom de notre scénario est celui de notre classe de test auquel "TestCase" a été supprimé.

Création de notre classe de test

Nous pouvons maintenant facilement créer notre classe de test :

 
@SpringApplicationContext("spring/applicationContext-all.xml")
public class NoPunchingBallTestCase extends AbstractJBehaveTestCase {
 
   @SpringBean("shop")
   private Shop shop;
 
   @Before
   public void initSteps() {
      addSteps(new ShopSteps(shop));
   }
}

Celle-ci bénéficie du chargement d'un contexte d'application Spring et de l'injection de notre magasin, via Unitils. Le cas présenté ici est simple, mais vous pouvez utiliser toutes les fonctionnalités que vous souhaitez.

Nous utilisons le magasin injecté pour instancier nos steps que nous donnons ensuite à notre classe mère. Libre à vous d'utiliser plusieurs classes contenant des étapes, ou de varier les étapes disponibles en fonction de votre test (d'où l'intérêt de créer une classe par scenario). Vous pouvez également effectuer une partie des instanciations dans votre classe mère.

Si vous souhaitez insérer des données en base et/ou la vérifier après le test, ou ajouter un quelconque comportement directement au niveau du test, vous pouvez facilement surcharger la méthode run :

 
@Test
@DataSet
@ExpectedDataSet
public void run() throws T hrowable {
   super.run();
}

Création des étapes

Le développement des étapes se fait comme d'habitude, en utilisant les instances fournies par la classe de test :

 
public class CustomerSteps {
 
   private ShopFacade shopFacade;
   private FCustomer customer;
 
   public CustomerSteps(ShopFacade shopFacade) {
      this.shopFacade = shopFacade;
   }
 
   [...]

 

Nous avons donc intégré JBehave avec les autres outils de tests que nous souhaitions utiliser. Libre à vous de faire différents essais pour le coupler avec d'autres frameworks. Si vous réussissez à intégrer JBehave d'une autre façon, n'hésitez pas à laisser un commentaire...

Notre exemple complet se trouve à cette adresse.

Mise à jour le Vendredi, 25 Mars 2011 08:04 Thomas Huguerre
 

Commentaires  

 
0 #4 bess 09-01-2013 13:05
Ce qui est étonnant c'est que le principe de
-je charge A
-je charge A', le modifie et l'enregistre

est assez courant dans les codes (bien que pas optimisé évidement) et ne pose jamais de pb ..
Citer
 
 
0 #3 bess 09-01-2013 12:40
Nous avons reproduit le bug à de nouvelles reprises.

Le cas (+précis) est le suivant
dans le @When je charge un objet "A" depuis la bdd grâce à des méthodes encapsulées dans des transactions automatiquement par le mécanisme de mon programme. le debug montre un identifiant java =70 (par exemple).

un peu plus loin, toujours dans le @When mon code fait de manière interne dans les couches basses :
- charge de mon objet A' (encore oui)
- modif objet A'
- save objet A'

C'est à ce moment que je rencontre l'erreur org.hibernate.HibernateExcept ion: illegally attempted to associate a proxy with two open Sessions qui est je penses indépendant de Unitils.

Nous regardons du côté du modèle de transactions qui engloberaient peut être pas assez Jbehave dans notre cas présent, créant (peut être) ce déphasage entre A et A'.
Citer
 
 
0 #2 Thomas Huguerre 02-01-2013 12:11
Salut,

de ce que tu expliques, je comprends que le problème ne vient pas de JBehave mais plutôt de Unitils, puisque le DataSet est fourni par ce dernier (à travers DBUnit d'ailleurs).

As-tu regardé de ce côté ?

Thomas
Citer
 
 
0 #1 bess 18-12-2012 18:27
déjà en premier lieux : félicitation !
Comme tu le soulignes, les exemples -autre que les hello world de base qui ne servent jamais- sont soit inexistant, soit ne fonctionnent pas ...


C'est relativement très chiant dès lors que ton code fait autre chose qu'un hello world :)

Après avoir lutté un bon mois je suis tombé sur ton blog qui m'a énormément débloqué. Reste un point de détail qui dois certainement provenir d'une mauvaise utilisation de Jbehave de ma part : je me tape une sale exception lors de mes accès en écriture aux bases de données (via DataSet) : org.hibernate.HibernateExcept ion: illegally attempted to associate a proxy with two open Sessions.

Pour moi la raison est soit une mauvaise gestion de la transaction, soit une mauvaise conf de Jbehave... Pourrais tu me confirmer que tu as réussit ce genre de manipulation (update multiple sur un même bean dans un même fichier *Step) ?

Merci d'avance :)
Citer
 

Ajouter un Commentaire


Code de sécurité
Rafraîchir