Le Behaviour Driven Development ou l'art d'écrire des tests que tout le monde comprend

Une des règles de base du TDD, le dévelopement piloté par les tests, est "avant d'écrire une ligne de code, écrire un test qui ne passe pas". Le ligne de code sert ainsi à faire passer le test. Cette approche permet d'écrire le code le plus simple possible, au sens où celui-ci n'a aucune ligne superflue, et de découvrir facilement le code à écrire puisque celui-ci découle du test.

Tout ceci serait très facile si le problème de l'inception de code n'était pas repoussé vers les tests. Certes, on découvre facilement le code à écrire. Encore faut-il trouver le bon test à faire passer !
Le BDD, le développement piloté par le comportement ("behaviour") apporte une réponse à ce problème en proposant une structure des tests basée sur le comportement observable des éléments du système.

Je ne vais pas ici développer le BDD en long et en large. Il y a de très bon sites pour cela, http://behaviour-driven.org/ entre autres.

Un développeur qui commence à pratiquer le TDD y trouve rapidement un bénéfice : sa confiance dans le code ne cesse de grandir car il met en place des centaines de garde-fous qui, à tout instant, le remettent dans le droit chemin.
Dans un 2ème temps, au delà de cet aspect de contrôle des régressions, un développeur va trouver dans le TDD un bon moyen pour documenter son code : les tests sont alors des exemples d'utilisation. Mais ce n'est que lorsqu'on approche le TDD comme technique de conception que l'on peut en mesurer toute la puissance.

Le BDD est l'évolution naturelle du TDD. Le pilotage par les tests permet littéralement de découvrir les interfaces du code à écrire. Et c'est là que le BDD entre en action en proposant un canevas : "étant donné un contexte, si un évènement survient, alors le système vérifie un certain nombre de conditions".

Prenons un exemple. Une fonctionnalité de mon système consiste à fournir le transfert d'argent d'un compte épargne vers un compte courant.
Pour le tester, je peux donc décrire un contexte :

  • Si le compte épargne est en crédit de 100
  • Et le compte courant est en crédit de 10

Puis initier un évènement :

  • Quand je transfère 20 vers le compte courant

Et enfin vérifier le nouvel état du système :

  • Le solde du compte épargne est 80
  • Et le solde du compte courant est 30

Un test facilement lisible va respecter ces étapes. Par exemple, en C# avec NUnit, je peux décrire mon contexte initial sous la forme d'un héritage de classes (une classe par condition initiale) :

namespace SiLeCompteEpargneEstEnCreditDe100 {
  public class C_SiLeCompteEpargneEstEnCreditDe100 {
    protected Compte leCompteEpargne;
    public void Given() { leCompteEpargne = new Compte() { Solde = 100 }; }
  }
}

namespace SiLeCompteEpargneEstEnCreditDe100.EtSiLeCompteCourantEstEnCreditDe10 {
  public class C_EtSiLeCompteCourantEstEnCreditDe10 : C_SiLeCompteEpargneEstEnCreditDe100 {
    protected Compte leCompteCourant;
    public void Given() {
      base.Given();
      leCompteCourant = new Compte() { Solde = 10 };
    }
  }
}


Pour que cette description de contexte compile, il me faut déclarer la classe Compte avec une propriété Solde : je viens de découvrir un "objet métier" de mon système.

public class Compte {
  public int Solde { get; set; }
}


A partir de là, je peux écrire un test qui lance l'évènement de transfert de compte à compte :

namespace SiLeCompteEpargneEstEnCreditDe100.EtSiLeCompteCourantEstEnCreditDe10 {
  [TestFixture]
  public class QuandJeTransfere20VersLeCompteCourant : C_EtSiLeCompteCourantEstEnCreditDe10 {
    [SetUp]
    public void When() {
      base.Given();
      leCompteEpargne.TransfererVers(leCompteCourant,20);
    }
  }
}


Si je veux compiler ce test, il me faut enrichir ma classe Compte : je viens de découvrir le besoin d'avoir une méthode de transfert.

public class Compte {
  public int Solde { get; set; }
  public void TransfererVers( Compte unAutreCompte, int montant ) {}
}


Maintenant, je peux vérifier mes conditions :

namespace SiLeCompteEpargneEstEnCreditDe100.EtSiLeCompteCourantEstEnCreditDe10 {
  [TestFixture]
  public class QuandJeTransfere20VersLeCompteCourant : C_EtSiLeCompteCourantEstEnCreditDe10 {
    [SetUp]
    public void When() {
      base.Given();
      leCompteEpargne.TransfererVers(leCompteCourant,20);
    }
    [Test]
    public void LeSoldeDuCompteEpargneEst80() {
      Assert.That(leCompteEpargne.Solde, Is.EqualTo(80) );
    }
    [Test]
    public void LeSoldeDuCompteCourantEst30() {
      Assert.That(leCompteCourant.Solde, Is.EqualTo(30) );
    }
  }
}


En lançant, l'exécution du test, je découvre rapidement qu'elles ne sont pas satisfaites mais, au moins, je sais pourquoi : mes résultats de test ne souffrent d'aucun manque de lisibilité !

BDDredbar.png

Pour faire passer mes tests, il me faut implémenter la méthode de transfert :

public class Compte {
  public int Solde { get; set; }
  private void AjouterAuSolde( int montant ) {
    Solde = Solde + montant;
  }
  public void TransfererVers( Compte unAutreCompte, int montant ) {
    if( Solde > 0 ) {
      unAutreCompte.AjouterAuSolde(montant);
      AjouterAuSolde(-montant);
    }
  }
}

Et voilà !

BDDgreenbar.png

A noter que si j'avais été très rigoureux, j'aurais rajouté mes conditions à vérifier une par une et j'aurais écrit le code correspondant au fur et à mesure.

A l'usage, on constate rapidement que l'écriture de tels tests avec un outil de TDD "classique" est fastidieuse. Plusieurs toolkits ont donc vu le jour pour s'adapter à ce genre d'approche "contexte/évènement/vérification". La page anglaise du BDD sur Wikipédia en propose une liste.

Voilà donc, en quelques mots, comment le BDD apporte :

  • un canevas d'écriture de test proche du langage humain : la spécification exécutable n'est pas très loin tout en gardant un outil proche du développeur !
  • une démarche de conception logicielle pilotée par les comportements attendus du système.

A mon avis, le BDD peut apporter bien plus que ça, notamment en termes de documentation du système développé et de communication entre les intervenants.
A suivre dans un prochain billet... Stay tuned !

claude

L'art d'écrire des tests que tout le monde comprend... Ca serait pas mieux de dire l'art de rendre exécutables des tests fonctionnels (exprimés par un client) ?

claude 9 novembre 2008 - 20:46
oaz

@claude,
Pour moi, "l'art de rendre exécutables des tests fonctionnels exprimés par un client", c'est plutôt ce que l'on appele le TDR (Test Driven Requirements) avec des outils tels que Fitnesse ou Greenpepper.

Le BDD, même si cela permet de se rapprocher -dans certains cas- d'une vision plus fonctionnelle, ça reste au niveau de la conception. J'admets que mon exemple avec un test qui porte sur du code métier peut prêter à confusion. Le BDD s'applique aussi bien à du code métier qu'à du code plus technique. Quand je dis "des tests que tout le monde comprend", le "tout le monde" signifie éventuellement des gens du métier (qui peuvent ainsi lire le test si celui-ci est un test de code métier) mais il signifie en premier lieu les autres développeurs pour qui l'intention du test est bien plus visible.

Sur la liste de discussion xp-france, il y avait eu un embryon de débat "évolution de TDD : BDD, TDR...". Je vois que Eric Lefèvre en a également parlé sur le blog Valtech. A mon avis, envisager le BDD uniquement comme une approche plus fonctionnelle des tests développeur est une erreur de compréhension de ce qu'est le BDD. TDR et BDD interviennent à des niveaux différents même si on observe une convergence lorsqu'il s'agit d'exprimer des tests fonctionnels.

oaz 10 novembre 2008 - 10:36
claude

Le BDD est une technique orientée comportement dont le sujet peut être le système ou un de ses composants. Les articles de Dan North abondent de références aux user stories et aux tests d'acceptation, le sujet y est donc le système (logiciel). Dans ton exemple aussi...
Sur l'aspect méthode de conception, on en reparle.

claude 10 novembre 2008 - 14:15
Oaz

Oui, le principal défaut de mon exemple est de n'être qu'un exemple !!!
Les tests développeur issus des histoires utilisateur sont importants. Je n'ai jamais dit l'inverse. Bien au contraire.

Mais posons la question autrement :

  • dans un développement piloté par les tests, peut-on se contenter de tests développeur issu d'histoires utilisateur ?
  • Si on ne peut pas s'en contenter, pourquoi ne pourrait-on pas appliquer le BDD sur les tests développeur non issus d'histoires utilisateur ?
Oaz 10 novembre 2008 - 17:58
ehsavoie

Je pense que le BDD permet l'utilisation d'un langage unique pour les tests quelque soit leur 'niveau':
On peut utiliser le BDD pour rendre le TDD intuitif => on est au niveau tests unitaires.
On peut aussi décrire les fonctions ou comportements du système avec la MOA = > on est au niveau des exigences fonctionnelles et des tests d'acceptabilité.
L'approche JBehave 2.0 est à mon humble avis exactement celle là en prenant des histoires d'utilisateur en format plein texte comme entrée. Il manque sûrement un éditeur sexy (quoique les wikis ...).
Mes 2 centimes :o)

Fil des commentaires de ce billet

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.