Traduction de l'article de Dan North: http://dannorth.net/introducing-bdd... paru pour la première fois dans Better Software Magazine en mars 2006 et publié ici avec son aimable autorisation.

J'avais un problème. Quand on utilise ou qu'on enseigne les pratiques agiles telles que le développement dirigé par les tests (TDD) sur des projets dans différents environnements, on rencontre toujours la même confusion et les mêmes difficultés de compréhension. Les programmeurs veulent savoir par où commencer, ce qu'il faut tester et ce qu'il ne faut pas tester, combien de tests doivent être exécutés en une fois, ce qui lance leurs tests et comment comprendre pourquoi un test échoue.

Plus on va profondément dans TDD, plus on a l'impression que le processus d'apprentissage est moins un processus d'amélioration progressive de sa maîtrise qu'une suite de tâtonnement et d'essais/erreurs. On arrive plus fréquemment à se dire : "si seulement quelqu'un m'avait dit ça avant" que "Eureka, une porte s'est ouverte". D'où la décision qu'il doit être possible de présenter le TDD d'une façon qui va directement aux bonnes choses et évite les écueils.

Une réponse est le Behaviour Driven Development (BDD). Il a émergé des pratiques agiles établies et est conçu pour les rendre plus accessibles et efficaces pour les équipes peu aguerries à la livraison de code agile. Au fil du temps, BDD a grandi pour englober une vision plus large de l'analyse agile et du test d'acceptation (de recette) automatisé.

Les noms des méthodes de test doivent être des phrases

Mon premier "bon sang, mais c'est bien sûr !" est survenu quand on m'a montré un utilitaire étonnamment simple, agiledox, écrit par mon collègue, Chris Stevenson. Cet outil prend en entrée une classe JUnit et affiche le nom des méthodes comme des vraies phrases, si bien qu'un cas de test qui ressemble à çà:

   public class CustomerLookupTest extends TestCase {
      testFindCustomerById() {
      ...
      }
  
      testFailsForDuplicateCustomers() {
      ...
      }
  ....
  }

sera rendu comme ceci:

  CustomerLookup
  - finds customer by id
  - fails for duplicate customers
  - ...

Le mot test est enlevé du nom de la classe et des noms des méthodes, et les noms des méthodes écrites en Camel Case sont convertis en texte normal. C'est tout ce que ça fait, mais l'effet est étonnant.

Les développeurs ont découvert que cela pouvait au moins produire leur documentation à leur place, si bien qu'ils ont commencé à écrire les méthodes de tests comme de vraies phrases. Mieux encore, ils ont découvert que s'ils écrivaient le nom d'une méthode dans le langage métier du domaine, les documents générés avaient du sens pour les utilisateurs fonctionnels, les analystes et les testeurs.

Un simple canevas de phrase assure que les méthodes de test restent correctement ciblées

Puis j'en vins à la convention de commencer les noms des méthodes de test avec le mot should (doit). Ce canevas de phrase - la classe doit faire quelque chose - signifie que vous ne pouvez définir un test que pour la classe actuelle. Ceci vous empêche de vous disperser. Si vous vous surprenez à écrire un test dont le nom ne correspond pas à ce canevas, cela indique que le comportement peut dépendre d'autre chose.

Par exemple, j'écrivais une classe qui valide les saisies dans une page. La plupart des champs sont des informations relatives aux clients - nom, prénom, etc. - mais il a aussi un champ pour la date de naissance et un autre pour l'âge. J'ai commencé à écrire un ClientDetailsValidatorTest avec des méthodes telles que testShouldFailForMissingSurname et testShouldFailForMissingTitle.

Puis j'en suis venu au calcul de l'âge et je suis entré dans un monde de règles métiers subtiles: que se passe-t'il si l'age et la date de naissance sont saisies toutes les deux mais ne correspondent pas ? Que se passe-t'il si la date de naissance est la date d'aujourd'hui ? Comment calculer l'âge si je n'ai que la date de naissance ? J'étais en train d'écrire des noms de méthodes de plus en plus encombrants pour décrire ce comportement quand je me suis dit qu'il fallait arrêter et trouver une autre solution. Ceci m'a conduit à introduire une nouvelle classe que j'ai appelé AgeCalculator possèdant sa propre classe de test AgeCalculatorTest. Tous les comportements relatifs aux calculs d'âge y ont été déplacés de telle sorte que le validateur n'avait plus besoin que d'un seul test relatif au calcul d'âge pour s'assurer qu'il interagissait proprement avec le calculateur.

Si une classe fait plus d'une chose, je considère généralement que c'est le signe que je dois introduire d'autres classes pour faire une partie du travail. Je définis le nouveau service comme une interface décrivant ce qu'il fait et je passe ce service via le constructeur de la classe:

  public class ClientDetailsValidator {
  
     private final AgeCalculator ageCalc;
     
     public ClientDetailsValidator(AgeCalculator ageCalc) {
     
        this.ageCalc = ageCalc;
     
     }
  
  }

Ce type de pratique consistant à lier des objets entre eux, connu comme injection de dépendances, est particulièrement utile en conjonction avec les bouchons (mock).

Un nom de test significatif est utile quand le test échoue

Après un moment, j'ai découvert que si je modifiais du code et que ça provoquait l'échec d'un test, je pouvais regarder le nom de la méthode de test pour identifier le comportement attendu du code. Typiquement, 3 situations peuvent se présenter:

  • j'ai introduit un bug. Honte sur moi. Solution: corriger le bug.
  • le comportement attendu est toujours pertinent mais s'est déplacé ailleurs. Solution: déplacer le test et éventuellement le modifier.
  • le comportement n'est plus correct - les prémices du système ont changé. Solution: supprimer le test.

Cette dernière situation est notamment susceptible de se produire dans les projets agiles quand votre compréhension évolue. Malheureusement, les novices en matière de TDD ont une peur innée de la suppression des tests, craignant que cela réduise la qualité de leur code.

Un aspect plus subtile du mot doit devient visible quand il est comparé avec ses alternatives plus formelles (va faire). Doit vous permet implicitement de vous confronter aux prémices du test: "Doit-il ? Vraiment ?". Ceci rend plus facile de déterminer si un test échoue à cause d'un bug que vous avez introduit ou simplement parce que vos hypothèses précédentes sur le comportement du système sont maintenant incorrectes.

"Comportement" est un mot plus utile que "Test"

Maintenant que j'ai un outil - agiledox - pour supprimer le mot "test" et un canevas pour chaque nom de méthode de test, il m'est subitement revenu que les gens qui ont une mauvaise compréhension de TDD reviennent presque toujours au mot "test".

Cela ne veut pas dire que l'activité de test n'est pas intrinsèque au TDD - la série résultante de méthode est un moyen efficace de s'assurer que votre code fonctionne. Cependant, si les méthodes ne décrivent pas de manière exhaustive le comportement de votre système, elles vous enferment dans un faux sentiment de sécurité.

J'ai commencé à utiliser le mot "comportement" à la place de "test" dans mes interventions en matière de TDD et j'ai découvert que non seulement ça correspondait très bien mais également que toute une catégorie de questions d'accompagnement a disparu comme par magie. J'ai maintenant des réponses à certaines des questions posées par TDD. Comment appeler votre test est facile - c'est une phrase décrivant le prochain comportement sur lequel vous allez porter votre intérêt. Jusqu'à quel point tester: autant que vous pouvez décrire le comportement dans une seule phrase. Quand un test échoue, utilisez simplement le processus décrit plus haut - soit vous avez introduit un bug, soit le comportement a été déplacé, soit le test n'est plus utile désormais.

J'ai trouvé que la modification de penser en comportement plutôt qu'en test était si profonde que j'ai commencé à parler de TDD comme du BDD ou Behaviour Driven Development: développement dirigé par le comportement.

JBehave met l'accent sur le comportement plutôt que sur les tests

A la fin 2003, j'ai décidé qu'il était temps d'investir mon argent - ou du moins mon temps - où se trouvait ma bouche. J'ai commencé à écrire un remplaçant de JUnit appelé JBehave qui supprime toute référence au test et la remplace avec un vocabulaire construit autour de la vérification du comportement. J'ai fait cela pour voir comment un tel framework évoluerait si je m'en tenais strictement à mes nouveaux mantras dirigés par le comportement. J'ai également pensé que ce sera un outil d'apprentissage de valeur pour introduire le TDD et le BDD sans la distraction causée par le vocabulaire basé sur les tests.

Pour définir le comportement d'une hypothétique classe CustomerLookup, j'écrirais par exemple une classe de comportement appelée CustomerLookupBehaviour. Elle contiendrait des méthodes qui commencent par le mot "doit" ("should"). Le lanceur de comportement instancierait la classe de comportement et invoquerait tour à tour chacune de ces méthodes de comportement comme JUnit le fait pour les tests. Il informera de l'avancement de l'opération et affichera un résumé à la fin.

Ma première étape était que JBehave puisse se vérifier lui-même. J'ai donc simplement ajouté les comportements qui lui permettait de s'exécuter lui-même. J'étais capable de migrer tous les tests JUnit en comportements JBehave et d'obtenir le même retour que celui fournit par JUnit.

Déterminer le prochain plus important comportement

J'ai ensuite découvert le concept de valeur métier. Bien sûr, j'avais toujours été conscient que j'écrivais un logiciel pour un objectif donné, mais je n'avais jamais vraiment pensé à la valeur du code au moment où je l'écrivais. Un autre collègue, l'analyste métier Chris Matt, me fit penser à la valeur métier dans le contexte du développement dirigé par le comportement.

Étant donné que j'avais en tête l'objectif de rendre JBehave auto-vérifié, j'ai trouvé que c'était un moyen très utile de rester concentré sur l'objectif que de me demander: quel est la prochaine plus importante chose que le système ne fait pas ?

Cette question nécessite de déterminer la valeur des fonctionnalités que vous n'avez pas encore implémentées et de les prioriser. Elle vous aide également à formuler le nom de la méthode de comportement: le système ne fait pas X (où X est un comportement plein de sens) et X est important, ce qui signifie qu'il doit faire X. Donc notre prochaine méthode de comportement est simplement:

  public void shouldDoX() {
  
  ...
  
  }

Maintenant, j'ai la réponse à une autre question TDD, à savoir où commencer.

Les exigences sont également des comportements

A ce stade, j'ai un framework qui m'aide à comprendre - et plus important, à expliquer - comment fonctionne le TDD et une approche qui évite tous les pièges que j'avais rencontrés.

Vers la fin 2004, alors que je décrivais ma nouvelle découverte en matière de vocabulaire basé sur le comportement à Matt, il me dit: "mais c'est exactement comme l'analyse". Il y eu un grand silence pendant que nous réfléchissions à cela puis nous décidâmes d'appliquer tout ce raisonnement dirigé par les tests pour définir des exigences. Si nous arrivions à développer un vocabulaire consistant pour les analystes, les testeurs, les développeurs et les gens du métier; nous ne serions pas loin d'arriver à éliminer une partie des ambiguïtés et des problèmes de communication qui surviennent quand des techniciens parlent à des fonctionnels.

BDD fournit un langage polyvalent pour l'analyse

A cette époque, Eric Evans a publié son best seller Domain-Driven Design (conception dirigée par le domaine). Dans ce livre, il décrit le concept de modélisation d'un système en utilisant un langage polyvalent basé sur le domaine métier de telle sorte que le vocabulaire métier arrive jusque dans le code.

Chris et moi avons alors réalisé que nous étions en train de définir un langage polyvalent pour le processus d'analyse lui-même ! Nous avions un bon point de départ. Pour les besoins courants dans les entreprises, il existait déjà un canevas d'histoire qui ressemblait à ce qui suit:

  En tant que {X}
  Je veux {Y}
  De façon que {Z}

où Y est une fonctionnalité, Z est le bénéfice ou la valeur de la fonctionnalité et X la personne (ou rôle) qui va en bénéficier. Sa force est qu'il vous oblige à identifier la valeur apportée par l'histoire au moment où elle est définie. Quand il n'y a pas de réelle valeur métier pour une histoire, elle se ramène souvent à quelque chose comme "... je veux {une fonctionnalité} de telle sorte que {je le fait, c'est tout, ok ?}". Ceci rend plus facile la réduction de la portée des exigences les plus ésotériques.

A partir de là, Matts et moi avons commencé à découvrir ce que tout testeur agile savait déjà: le comportement d'une histoire est simplement un critère de validation - si le système remplit tous les critères de validation, il se comporte correctement; s'il ne le fait pas, il ne se comporte pas correctement. Donc nous avons créé un canevas destiné à capturer les critères de validation d'une histoire.

Le canevas doit être suffisamment souple pour qu'il ne semble pas artificiel ou ne contraigne pas trop les analystes mais suffisamment structuré de façon à ce que nous puissions diviser l'histoire en constituants et les automatiser. Nous avons commencé à décrire les critères de validation en termes de scénarios, sous la forme suivante:

  Etant donné un contexte initial (les acquis ou Given)
  Lorsqu'un événement survient (When),
  Alors on s'assure de l'obtention de certains résultats (Then)

Pour illustrer cela, utilisons l'exemple classique du distributeur de billets. L'une des cartes d'histoire pourrait ressembler à ce qui suit:

  +Titre: le Client retire du liquide+
  En tant que Client,
  Je veux retirer du liquide d'un distributeur,
  De telle sorte que je n'ai pas à faire la queue à la banque.

Bien. Comment savoir si nous avons livré cette histoire ? Il y a plusieurs scénarios à envisager: le compte peut être crédité, le compte peut être débiteur mais dans la limite du découvert autorisé, ou bien cette limite peut être atteinte. Bien sûr, il y aura d'autres scénarios tel que le compte est crédité mais le retrait le rend débiteur au-delà de ce qui est autorisé ou tel que le distributeur est en rupture de billets.

En utilisant le canevas Étant donné - Lorsque - Alors, les 2 premiers scénarios pourraient ressembler à ce qui suit:

  +Scénario 1: le compte est crédité
  Étant donné que le compte est crédité
  Et que la carte est valide
  Et que le distributeur contient du liquide
  Lorsque le client demande du liquide
  Alors on s'assure que son compte est débité
  Et que le liquide est distribué
  Et que la carte est rendue

Notez l'utilisation de "et" pour connecter plusieurs pré-requis ou plusieurs résultats de manière naturelle.

  +Scénario 2: le compte est débiteur au-delà de la limite du découvert autorisé+
  Étant donné que le compte est débiteur
  Et que la carte est valide
  Lorsque le client demande du liquide
  Alors on s'assure qu'un message de refus est affiché
  Et que le liquide n'est pas distribué
  Et que la carte est rendue

Les deux scénarios sont basé sur le même événement et cet événement possède des pré-requis et des résultats communs. Nous voulons capitaliser cela en réutilisant ces pré-requis, ces événements et ces résultats. Les critères d'acceptation doivent être exécutables

Les parties du scénario - pré-requis, événements et résultats - sont assez petits pour être directement représentés sous forme de code. JBehave définit un modèle objet qui nous permet de faire directement correspondre les parties du scénario aux classes Java.

Vous écrivez une classe représentant chaque pré-requis:

 public class CompteCredite implements Given {
  
    public void setup(World world) {
  
       ...
  
    }
  
  }
  
  public class CarteEstValide implements Given {
  
    public void setup(World world) {
  
       ...
  
    }
  
  }

et une pour l'événement:

  public class LeClientDemandeDuLiquide implements Event {
  
    public void seProduitDans(World world) {
  
       ...
  
    }
  
  }

et de même pour les résultats. JBehave relie ensuite tout ces éléments et les exécute. Il crée un monde ("world") qui est simplement là où sont enregistrés vos objets et le passe aux pré-requis pour qu'ils le peuplent avec un état connu. JBehave indique ensuite aux événements de se produire dans ce monde de façon à correspondre au comportement réel du scénario. Enfin, il réalise tous les contrôles sur les résultats que nous avons défini pour l'histoire.

Avoir une classe qui représente chaque partie nous permet de réutiliser ces parties dans d'autres scénarios ou histoires. Au début, ces parties sont implémentées en utilisant des bouchons (mock) pour que le compte soit crédité ou que la carte soit valide. Ceci constitue le point de départ pour implémenter le comportement. Au fur et à mesure que vous implémentez l'application, les pré-requis et les résultats seront modifiés pour utiliser les classes réelles que vous aurez implémentées si bien qu'au moment où le scénario est achevé, ils seront devenus des tests fonctionnels appropriés de bout en bout.

Le présent et le futur du BDD

Après une brève interruption, JBehave est de nouveau en développement actif. Le noyau est presque complet et robuste. La prochaine étape est l'intégration avec les IDE JAVA populaires tels que IntelliJ? IDEA et Eclipse.

Dave Astels a activement fait la promotion du BDD. Son blog et les nombreux articles publiés ont provoqué une rafale d'activité, plus particulièrement le projet rspec pour produire un framework BDD en langage Ruby. J'ai commencé à travailler sur rbehave qui sera une implémentation de JBehave en Ruby.

Nombre de mes collègues ont utilisé les techniques BDD avec succès sur divers projets réels. Le lanceur d'histoire JBehave - la partie qui vérifie les critères de validation - est en développement actif.

La vision est d'avoir un éditeur collaboratif de telle sorte que les fonctionnels et les testeurs puissent capturer les histoires dans un éditeur de texte habituel qui puisse générer le squelette des classes de comportement, et tout cela dans le langage du domaine métier. BDD évolue avec l'aide de nombreuses personnes: je leur en suis à toutes extrêmement reconnaissant.