Accueil
Rechercher:
sur developpez.com sur les forums
Forums | Tutoriels | F.A.Q's | Participez | Hébergement | Contacts
Club Emploi Blogs   TV   Dév. Web PHP XML Python Autres 2D-3D-Jeux Sécurité Windows Linux PC Mac
Accueil Conception Java DotNET Visual Basic  C  C++ Delphi MS-Office SQL & SGBD Oracle  4D  Business Intelligence
FORUMS CONCEPTION TUTORIELS CONCEPTION F.A.Qs CONCEPTION UML MERISE LIVRES CONCEPTION

Tutoriel : Développement Dirigé par les Tests

Date de publication : 10 juillet 2007 , Date de mise à jour : 09 août 2007

Par Bruno Orsier (Site Web)
 

Cette page présente un tutoriel sur le développement dirigé par les tests (ou Test Driven Development en anglais). Le tutoriel présente les principes de cette méthode, et ensuite l'illustre pas à pas sur un exemple concret : la recherche de toutes les solutions du problème des pentaminos. Les principaux outils utilisés sont Visual C# Express et Nunit. Les notions de couverture de code, de complexité cyclomatique sont également abordées.
A titre indicatif, le temps nécessaire pour suivre le tutoriel est de l'ordre d'une dizaine d'heures. N'hésitez pas à me faire part de vos impressions : vos remarques pourront être prises en compte dans de futures versions.

Historique
I. Introduction
II. Principes du TDD
III. Premiers pas
III-A. Mise en place du projet
III-B. Premier test
III-C. Notre liste de travail initiale
IV. Modélisation des pentaminos
V. Modélisation du plateau
V-A. Premiers tests
V-B. Gestion des chevauchements
V-C. Utilisation d'une solution connue
V-D. Principe de l'algorithme
V-E. Dernières briques
VI. Implémentation de l'algorithme
VI-A. Un test fonctionnel
VI-B. Perfectionnements
VI-B-1. Réduction du nombre de solutions
VI-B-2. Autres dimensions de plateaux
VII. Discussion
VII-A. Intérêts du TDD
VII-B. Couverture de code
VII-C. Complexité cyclomatique
VII-D. Possibilités de Nunit
VII-E. Limites du TDD
VIII. Conclusion
Bibliographie
Remerciements


Historique

Historique des versions
  1. mai-juillet 2007 : création du document
  2. 10 juillet 2007 : prise en compte de remarques de rédacteurs de Developpez.com ; ajout du perfectionnement concernant les dimensions du plateau
  3. 09 août 2007 : passage au gabarit du site Developpez.com ; lien vers la liste d'anti-patrons de tests unitaires

I. Introduction

Ce tutoriel propose la mise en oeuvre d'un développement dirigé par les tests (Test Driven Development - TDD) sur un cas concret selon les principes exposés par Kent Beck dans son livre Test-Driven Development: By Example.

L'exemple donné par Kent Beck (un calculateur multi-monnaies) est trompeusement simple, si bien que des lecteurs sous-estiment parfois l'intérêt du TDD. Aussi ce tutoriel s'appuie sur un sujet qui paraîtra plus difficile, afin de mieux montrer l'apport du TDD.

Par rapport à d'autres tutoriels qui existent déjà, comme
nous allons essayer de montrer la mise en pratique de cette démarche de A à Z, afin de bien insister sur la discipline particulière que propose Kent Beck.

Le sujet que nous allons traiter est le calcul de toutes les solutions possibles du jeu des pentaminos (voir wikipedia).

(image sous License Creative Commons ShareAlike 1.0)
Les douze pentaminos, composés chacun de 5 cases.
L'objectif est de placer ces douze pentaminos sur un rectangle de 6 par 10 cases, sans aucun trou ni chevauchement. Grâce à wikipedia, nous savons d'avance qu'il y a 2339 solutions uniques (4 fois plus en comptant les solutions symétriques), et dans ce tutoriel nous souhaitons pouvoir les afficher toutes.

Réaliser un tel programme paraît souvent difficile, notamment en raison du caractère "géométrique" du problème, et c'est à ce titre qu'il nous intéresse comme support d'introduction du TDD. De plus ce problème n'est pas uniquement ludique, c'est un cas particulier d'un problème de satisfaction de contraintes qu'un développeur peut être amené à rencontrer ; il est donc intéressant d'en connaître les principes de résolution.

Le projet sera réalisé en Visual C# 2005 Express Edition, téléchargeable gratuitement sur le site de Microsoft (http://www.microsoft.com/france/msdn/vstudio/express/vcsharp/telechargez.mspx) et avec l'aide de l'outil de tests unitaires Nunit, également téléchargeable gratuitement (http://www.Nunit.org/ - prendre la version win .net 2.0).


II. Principes du TDD

L'objectif du TDD est de produire du "code propre qui fonctionne". Pour cela, deux principes sont mis en oeuvre :

  1. un développeur écrit du code nouveau seulement lorsqu'un test automatisé a échoué
  2. toute duplication de code (ou plus généralement d'information, ou de connaissances) doit être éliminée. L'acronyme anglais DRY (Do not Repeat Yourself) peut être utilisé comme moyen mnémotechnique pour cette phase très importante.
Ces deux principes doivent être strictement respectés, même s'ils paraissent difficiles ou bizarres dans un premier temps. Bien comprendre le TDD suppose en effet de respecter strictement la discipline imposée par le cycle décrit ci-dessous.

Bien que simples, ils ont diverses implications :
  • nous allons concevoir notre code de manière incrémentale, en ayant toujours du code en état de marche, de telle sorte que ce code nous fournisse de l'information pour prendre de nombreuses petites décisions au cours de notre développement.
  • nous devons écrire nos propres tests, parce que nous ne pouvons pas attendre de nombreuses fois par jour qu'une autre personne le fasse
  • notre environnement de développement doit fournir une réponse ultra rapide en cas de petits changements
  • notre code doit être composé d'éléments très cohérents et très peu couplés, afin de rendre le test facile.
Le travail se fait en trois phases. Les deux premières sont nommées d'après la couleur de la barre de progrès dans les outils comme Nunit :

  1. ROUGE: écrire un petit test qui échoue, voire même ne compile pas dans un 1er temps
  2. VERT : faire passer ce test le plus rapidement possible, en s'autorisant si besoin les "pires" solutions : S'il existe une solution propre, simple et immédiate, réalisez-la Si une telle solution prend plus d'une minute, notez la et revenez au problème principal : avoir une barre de progrès verte en quelques secondes
  3. REMANIEMENT (refactoring en anglais) : éliminer absolument toute duplication apparue durant les étapes 1 et 2.
Pour réaliser l'étape 2 ci-dessus, il y a trois stratégies :
  1. Simulation : retourner une constante, puis remplacer progressivement ces constantes avec des variables afin d'obtenir le code réel
  2. Implémentation évidente : taper directement la bonne solution
  3. Triangulation : avoir deux exemples du résultat recherché, et généraliser.
Le cycle de travail en TDD est donc le suivant :
  1. ajouter rapidement un nouveau test
  2. Exécuter tous les tests, et constater l'échec du nouveau : ROUGE
  3. Faire un petit changement
  4. Exécuter tous les tests, et constater qu'ils passent : VERT
  5. Remanier le code pour éliminer toute duplication : REMANIEMENT
Il est essentiel que ce cycle se réalise très rapidement, en quelques minutes tout au plus. Si la réalisation du cycle prend des dizaines de minutes, il est probable que vous soyez en train d'essayer de réaliser un pas trop important ; il est alors souhaitable d'essayer d'attaquer une étape moins ambitieuse. Comme nous le verrons, la particularité du TDD est de réaliser des étapes qui peuvent paraître minuscules à des développeurs chevronnés, certains parlent même de micro-incréments de code.

Le cycle complet est résumé par le diagramme ci-dessous, avec l'ordre de grandeur du temps à consacrer à chaque phase.


III. Premiers pas


III-A. Mise en place du projet

Après avoir téléchargé et installé Visual C# Express et Nunit, nous créons une application console (Menu Fichier, Nouveau Projet, et Application Console) appelées Pentaminos. Ensuite il faut ajouter la référence Nunit-framework au projet, depuis l'explorateur de solution Visual C# :

Sauvegarder la solution (menu Fichier, Sauver Tous) puis la construire (F6).

Lancer Nunit GUI, puis ouvrir l'executable produit par Visual C#, il se trouvera dans Pentaminos\bin\Release\Pentaminos.exe. Nunit GUI affiche alors notre projet, avec aucun test pour l'instant :

A partir de là nous allons passer constamment de Visual C# à Nunit : après de petites modifications dans le code C# (et reconstruction de la solution par F6) nous passerons dans Nunit GUI pour appuyer sur le bouton Run, et examiner la couleur de la barre de progression située en dessous de Run.


III-B. Premier test

Afin de vérifier le fonctionnement de nos outils, ajoutons un test élémentaire. Il s'agit de vérifier que le programme peut retourner une description. Les principes du TDD nous imposent d'écrire le test d'abord. Donc nous ajoutons dans le fichier Program.cs le code suivant :

    [TestFixture]
    public class TestProgram
    {
        [Test]
        public void TestDescription()
        {
            Assert.That(Program.Description.Length, Is.GreaterThan(0));
        }
    }
Nous remarquons ci-dessus plusieurs éléments apportés par Nunit:
  • l'attribut TestFixture, qui permet d'indiquer qu'une classe est une classe de test, et qu'elle sera visible par Nunit
  • l'attribut Test, qui permet d'indiquer qu'une méthode d'une classe de test est un test. Chacune de ces méthodes est exécutée de manière complètement indépendante: en effet, Nunit va créer une nouvelle instance de la classe de test pour exécuter chacune des méthodes de tests.
  • Une assertion, sous la forme Assert.That( , ). Dans ce tutoriel nous utilisons une forme très récente de ces assertions, introduite dans Nunit 2.4. Celle nouvelle forme est plus lisible et plus évolutive que la forme dite classique, dans laquelle l'assertion aurait pris la forme :

Assert.Greater(Program.Description.Length,0) ;
Et il faut également insérer les déclarations suivantes au début du fichier :

using Nunit.Framework;
using Nunit.Framework.Constraints;
using Nunit.Framework.SyntaxHelpers;
A ce stade, notre code ne compile pas, ce qui est prévisible. Il faut ajouter une propriété description à la classe Program :

static public string Description
{
   get { return ""; }
}
C'est le minimum que nous pouvons faire pour pouvoir compiler. Nunit peut alors nous montrer l'exécution de ce test, qui bien sûr échoue, et donc la barre est ROUGE :

Observez également que Nunit donne des informations intéressantes sur la cause de l'échec du test.

Le minimum pour faire passer le test est de retourner la valeur attendue :

get { return "Pentaminos"; }
La barre Nunit devient alors verte.

La dernière étape du cycle TDD nous impose de supprimer toute duplication de code ; il n'y en a pas ici, donc nous avons fini le cycle.

Avec cet exemple élémentaire, nous avons donc pu :
  • vérifier le fonctionnement de nos outils
  • observer un cycle complet de TDD : compilation, barre ROUGE, barre VERTE, remaniement du code

III-C. Notre liste de travail initiale

A ce stade, nous ne savons pas nécessairement comment programmer la recherche des solutions, mais cela ne doit pas nous bloquer. En effet Kent Beck recommande de mettre une point une liste de toutes les actions de programmation simples et pertinentes que nous pouvons imaginer pour progresser sur ce projet. Voici une liste de départ élaborée en quelques minutes de réflexion :

info Ici nous avons utilisé un format particulier (une "mind map"), mais n'importe quel support peut être utilisé, à commencer par le papier ou un tableau blanc.
Partant de cette liste, nous pouvons commencer à travailler en TDD sur l'action qui nous paraît la plus élémentaire possible (rappelez-vous que nous allons chercher à faire des micro-incréments de code). Commençons par la modélisation des pentaminos.


IV. Modélisation des pentaminos

En tenant compte de toutes les rotations et symétries possibles, les 12 pentaminos peuvent présenter en tout 63 variantes. Nous allons simplement réutiliser une modélisation de ces pentaminos déjà faite par David Eck (avec sa permission), sous la forme suivante :

{
           { 1, 1,2,3,4 },         // This array represents everything the program
	   { 1, 10,20,30,40 },     // knows about the individual pentominos.  Each
	   { 2, 9,10,11,20 },      // row in the array represents a particular
	   { 3, 1,10,19,20 },      // pentomino in a particular orientation.  Different
	   { 3, 10,11,12,22 },     // orientations are obtained by rotating or flipping
	   { 3, 1,11,21,22 },      // the pentomino over.  Note that the program must
	   { 3, 8,9,10,18 },       // try each pentomino in each possible orientation,
	   { 4, 10,20,21,22 },     // but must be careful not to reuse a piece if
	   { 4, 1,2,10,20 },       // it has already been used on the board in a
	   { 4, 10,18,19,20 },     // different orientation.
	   { 4, 1,2,12,22 },       //     The pentominoes are numbered from 1 to 12.
	   { 5, 1,2,11,21 },    // The first number on each row here tells which pentomino
	   { 5, 8,9,10,20 },       // that line represents.  Note that there can be
	   { 5, 10,19,20,21 },     // up to 8 different rows for each pentomino.
	   { 5, 10,11,12,20 },     // some pentominos have fewer rows because they are
	   { 6, 10,11,21,22 },     // symmetric.  For example, the pentomino that looks
	   { 6, 9,10,18,19 },      // like:
	   { 6, 1,11,12,22 },      //           GGG
	   { 6, 1,9,10,19 },       //           G G
	   { 7, 1,2,10,12 },       //
	   { 7, 1,11,20,21 },      // can be rotated into three additional positions,
	   { 7, 2,10,11,12 },      // but flipping it over will give nothing new.
	   { 7, 1,10,20,21 },      // So, it has only 4 rows in the array.
	   { 8, 10,11,12,13 },     //     The four remaining entries in the array
	   { 8, 10,20,29,30 },     // describe the given piece in the given orientation,
	   { 8, 1,2,3,13 },        // in a way convenient for placing the piece into
	   { 8, 1,10,20,30 },      // the one-dimensional array that represents the
	   { 8, 1,11,21,31 },      // board.  As an example, consider the row
	   { 8, 1,2,3,10 },        //
	   { 8, 10,20,30,31 },     //           { 7, 1,2,10,19 }
	   { 8, 7,8,9,10 },        //
	   { 9, 1,8,9,10 },        // If this piece is placed on the board so that
	   { 9, 10,11,21,31 },     // its topmost/leftmost square fills position
	   { 9, 1,2,9,10 },        // p in the array, then the other four squares
	   { 9, 10,20,21,31 },     // will be at positions  p+1, p+2, p+10, and p+19.
	   { 9, 1,11,12,13 },      // To see whether the piece can be played at that
	   { 9, 10,19,20,29 },     // position, it suffices to check whether any of
	   { 9, 1,2,12,13 },       // these five squares are filled. 
	   { 9, 9,10,19,29 },      
	   { 10, 8,9,10,11 },      
	   { 10, 9,10,20,30 },    
	   { 10, 1,2,3,11 },
	   { 10, 10,20,21,30 },
	   { 10, 1,2,3,12 },
	   { 10, 10,11,20,30 },
	   { 10, 9,10,11,12 },  
	   { 10, 10,19,20,30 },
	   { 11, 9,10,11,21 },
	   { 11, 1,9,10,20 },   
	   { 11, 10,11,12,21 },
	   { 11, 10,11,19,20 }, 
	   { 11, 8,9,10,19},
	   { 11, 1,11,12,21 },
	   { 11, 9,10,11,19 },
	   { 11, 9,10,20,21 },
	   { 12, 1,10,11,21 },
	   { 12, 1,2,10,11 },
	   { 12, 10,11,20,21 },
	   { 12, 1,9,10,11 },  
	   { 12, 1,10,11,12 }, 
	   { 12, 9,10,19,20 },
	   { 12, 1,2,11,12 },
	   { 12, 1,10,11,20 }   
        };
//         by:  David J. Eck
//              Deparment of Mathematics and Computer Science
//              Hobart and William Smith Colleges
//              Geneva, NY   14456
//              Email:  eck@hws.edu
//
Toutefois, le TDD nous interdit d'écrire du code avant d'avoir un test. Quel test écrire ici ? Eh bien nous pourrions vérifier qu'il y a bien 63 éléments en tout (une erreur de copier/coller est toujours possible). Donc le test pourrait être

Assert.That(ListeDePentaminos().Count, Is.EqualTo(63));
En fait il faut que cette liste soit portée par une classe, donc nous choisissons d'introduire une classe FabriqueDePentaminos comme ci-dessous:

Assert.That(FabriqueDePentaminos.ListeDePentaminos().Count, Is.EqualTo(63));
Le test étant défini, nous avons maintenant la permission d'écrire du code : ajoutons un fichier Pentaminos.cs au projet, fichier qui contiendra la classe de test

    [TestFixture]
    public class TestListePentaminos
    {
        [Test]
        public void TestTotalPentaminos()
        {
            Assert.That(FabriqueDePentaminos.ListeDePentaminos().Count, Is.EqualTo(63));
        }
    }
ainsi que tout ce qui concernera la définition des pentaminos et de leur fabrique.

Voici le code minimum pour parvenir à compiler, et passer à la barre ROUGE :

    class Pentamino
    {
    }

    class FabriqueDePentaminos
    {
        static public List<Pentamino> ListeDePentaminos()
        {
            return new List<Pentamino>();
        }
    }
Pour avoir la barre verte, on peut écrire le code suivant :

static public List<Pentamino> ListeDePentaminos()
{
	List<Pentamino> liste = new List<Pentamino>();
	for (int i = 0; i < 63; i++)
	{
		liste.Add(new Pentamino());
	}
	return liste;
}
warning Attention, 63 est dupliqué ! La troisième phase du cycle nous impose de supprimer toute duplication, ce que nous pouvons faire en introduisant une constante portée par FabriqueDePentaminos :

public const int NombreDeVariantes = 63;
Le test suivant va nous forcer à remplir effectivement la structure de Pentomino. Par exemple, vérifions que le 3ème élément de la liste est bien le pentamino X, si nous avons bien compris la modélisation ; le test est alors :

[Test]
public void TestPositionDuPentominoX()
{
   Pentamino x =  FabriqueDePentaminos.ListeDePentaminos()[2] ;

   Assert.That(x.Decalage[0], Is.EqualTo(9),
	"le premier décalage de X est incorrect");
   Assert.That(x.Decalage[1], Is.EqualTo(10), 
	"le deuxième décalage de X est incorrect");
   Assert.That(x.Decalage[2], Is.EqualTo(11), 
	"le troisième décalage de X est incorrect");
   Assert.That(x.Decalage[3], Is.EqualTo(20), 
	"le quatrième décalage de X est incorrect");
   Assert.That(x.Variante, Is.EqualTo(2), 
	"la Variante de X est incorrecte");
}
Ce test comprend plusieurs assertions à la suite : ceci n'est pas souhaitable en général, pour les raisons suivantes :
  • en cas d'échec, il est plus difficile d'identifier quel test a échoué
  • le test s'arrête dès que l'une des assertions échoue, ce qui nous prive d'informations complémentaires sur le code qui est testé ; en pratique, dans des cas plus compliqués, nous serons souvent obligés de commenter l'assertion qui a échoué, afin de voir si les autres assertions passent ou pas.
Ici nous choisissons tout de même grouper ces 5 assertions dans un seul test, et nous rendons plus évident le test qui échouera en mettant une chaîne de caractères dans l'assertion.

Ce test nous conduit à définir l'interface de Pentamino comme suit :

        public int[] Decalage = new int[4];
        public int Variante;
ce qui nous permet d'arriver à la barre rouge. Pour avoir la barre verte, il faut maintenant remplir les pentaminos avec leur description, donc :

  • ajouter un constructeur,
  • insérer un tableau à deux dimensions qui contiendra la description interne des pentaminos (celle obtenue de D. Eck)
  • parcourir ce tableau dans la méthode ListeDePentaminos
Ces portions ne code ne sont pas reproduites ici, voir directement le fichier source Pentamino.cs.

On peut alors remarquer qu'il est possible de remplacer la constante 63 par la dimension du tableau interne, de la manière suivante :

        static public int NombreDeVariantes
        {
            get { return DescriptionsInternes.GetUpperBound(0) + 1; }
        }
Ceci permet les remarques suivantes :
  • ce changement peut être fait en toute sécurité, car les tests passent toujours tous
  • nous avons éliminé une autre duplication, moins évidente. Plus tard s'il faut modifier le nombre de variantes de pentaminos, il suffirait d'agir à un seul endroit, le tableau des descriptions internes.
A ce stade nous avons une première modélisation des pentaminos, couverte par deux tests qui sont présentés ci-dessous :

Et nous pouvons avancer sur le point suivant, la modélisation du plateau qui accueillera les pentaminos.


V. Modélisation du plateau

Il y a de nombreuses manières d'aborder ce point ; par exemple il est tentant de réfléchir à la structure interne de ce plateau. Allons-nous utiliser un tableau à deux dimensions, ou bien à une dimension comme la description interne des pentaminos le suggère ? Laquelle sera la plus pratique ? La plus efficace ?

Toutefois nous devons nous laisser guider par des tests avant de coder quoi que ce soit. Imaginer de tels tests est souvent très difficile pour les débutants en TDD ; pour les aider, une autre manière de voir les tests est d'imaginer qu'ils représentent des exemples de ce que l'on veut faire. Ceci va nous conduire à définir en premier une interface (un contrat) plutôt qu'une structure interne.

Voici des exemples de ce que nous souhaitons faire avec le plateau :
  • pouvoir ajouter un pentamino
  • ne pas pouvoir ajouter deux fois le même pentamino
  • ne pas permettre de chevauchement de pentamino
  • les pentaminos ajoutés ne devront pas déborder du plateau
  • savoir si une solution a été trouvée
  • pouvoir enlever un pentamino
  • pouvoir afficher un plateau (en mode console)

V-A. Premiers tests

Commençons donc par écrire un test, qui bien sûr ne compilera même pas :

Assert.That(plateau.Ajoute(I));
pour traduire l'intention d'ajouter un pentamino. Pour compiler, il faut alors ajouter une nouvelle classe Plateau (et un nouveau fichier Plateau.cs au projet), et le code suivant :

    class Plateau
    {
        public Boolean Ajoute(Pentamino pentamino)
        {
            return false;
        }
    }

    [TestFixture]
    public class TestPlateau
    {
        [Test]
        public void TestAjoutPentamino()
        {
            Plateau plateau = new Plateau();
            Pentamino I = FabriqueDePentaminos.ListeDePentaminos()[0] ;
            Assert.That(plateau.Ajoute(I));
        }
    }
Ce qui permet de compiler et d'arriver à la barre rouge. Le minimum pour arriver à la barre verte est alors de changer false en true dans la méthode Ajoute.

Ces étapes minimalistes peuvent paraître superflues. Il n'en est rien. Ces petites étapes permettent de valider l'outil de test avant d'écrire le véritable code de production. En effet sur un projet réel, qui comportera des milliers de tels petits tests, il n'y a rien de pire qu'un test qui est vert tout de suite : il peut être vert par hasard, ou par erreur de construction (la condition de l'assertion est toujours vérifiée). On peut également être en train de travailler sur un autre test que celui que l'on imagine... En d'autres termes, ce qui valide un test, ce n'est pas la barre verte, c'est l'observation du passage de la barre rouge à la barre verte.

Ayant notre barre verte, et n'ayant a priori pas introduit de duplication, il faut avoir un autre test avant d'écrire plus de code ! Tout simplement

[Test]
public void TestAjoutPentaminoSansRepetition()
{
	Plateau plateau = new Plateau();
	Pentamino I = FabriqueDePentaminos.ListeDePentaminos()[0];
	plateau.Ajoute(I);
	Assert.That(plateau.Ajoute(I), Is.False);
}
Ce code compile, et donne la barre rouge comme attendu. Il introduit également des duplications dans le code de test, point à régler dès que la barre verte sera obtenue. Pour obtenir la barre verte, impossible de continuer à changer des "true" en "false" : ce cas est un exemple de triangulation, où nous sommes contraints par deux tests au moins à écrire du code (ce qui oblige à généraliser).

Ici une solution assez simple est possible, donc écrivons-là directement pour avoir la barre verte :

private Boolean[] VariantesDejaAjoutees = new Boolean[12] ;

public Boolean Ajoute(Pentamino pentamino)
{
	if (VariantesDejaAjoutees[pentamino.Variante])
	{
		return false;
	}
	else
	{
		VariantesDejaAjoutees[pentamino.Variante] = true;
		return true;
	}
}
Il faut maintenant passer à la duplication de code présente dans la classe de test, où les deux méthodes comportent des initialisations communes. Nunit permet de grouper ces initialisations dans une méthode Setup comme suit :

        private Plateau plateau;
        private Pentamino I;

        [SetUp]
        public void SetUp()
        {
            plateau = new Plateau();
            I = FabriqueDePentaminos.ListeDePentaminos()[0];
         }
Cette méthode SetUp est automatiquement appelée par Nunit avant l'exécution de chaque test (tout comme une méthode TearDown est appelée après, afin de libérer des resources si besoin).

Cela permet de supprimer la duplication, comme d'habitude en toute sécurité puisque la barre reste verte après cette opération.

A ce stade nous avons confiance que la méthode Ajoute gérera correctement les ajouts de pentaminos sans permettre d'ajouter deux fois le même, ni deux variantes du même pentamino, donc nous n'écrivons pas plus de tests sur ce point.

Maintenant nous pouvons déjà traiter l'identification d'une solution trouvée, en considérant qu'il suffira qu'un pentamino de chaque sorte ait été posé.

Voici un premier test

       [Test]
       public void TestSolutionTrouveePlateauVide()
       {
            Assert.That(plateau.SolutionTrouvee, Is.False);
       }
pour compiler et passer à la barre rouge :

       public Boolean SolutionTrouvee
       {
            get { return true; }
       }
Pour arriver à la barre verte, il y a deux options :
  1. Changer true en false (barre verte) - mais cette solution ne marche pas dans tous les cas, il faut alors ajouter un test supplémentaire, du type ajouter les 12 pentaminos d'une solution connue, et vérifier la valeur de SolutionTrouvee. C'est la méthode de triangulation, qui continue à nous faire faire de tout petits pas.
  2. Considérer que l'implémentation est évidente, et sans risque, et faire un pas un peu plus grand.
Prenons l'implémentation évidente, afin de montrer que le développeur a le choix de la taille des pas :

       public Boolean SolutionTrouvee
        {
            get { return (TotalVariantesDejaAjoutees == FabriqueDePentaminos.NombreDePentaminos) ; }
        }
avec de plus :
  • le code nécessaire à la déclaration et l'initialisation de TotalVariantesDejaAjoutees
  • l'incrémentation de TotalVariantesDejaAjoutees dans la méthode Ajoute
  • l'extraction d'une méthode privée Pose, afin de rendre indissociables les deux opérations qu'il faut maintenant faire quand un pentamino est ajouté. Noter que nous ne cherchons pas à tester cette méthode privée : en TDD on se contente de tester l'interface publique d'une classe, afin d'éviter que le test ne se fragilise en devenant dépendant d'une structure interne susceptible d'évoluer.
  • la déclaration d'une constante NombreDePentaminos dans la fabrique de pentaminos, afin de supprimer la duplication du nombre 12.
Voici donc un pas plus important, qui n'est pas sans risque ; un débutant programmeur aurait certainement intérêt à prendre l'option a) ci-dessus.


V-B. Gestion des chevauchements

La barre verte est maintenant obtenue, et nous passons à la gestion des chevauchements.

Voici un exemple (et donc un test) de ce que nous aimerions faire :
  • ajouter le I à un certain endroit
  • vérifier qu'il n'est pas possible d'ajouter un autre pentamino, disons le X, au même endroit.
En essayant d'écrire un test, plusieurs difficultés apparaissent :
  • la méthode Ajoute ne permet pas de préciser un "endroit"
  • définir la notion d' "endroit" ne paraît pas possible sans se poser la question de la structure interne décrivant le plateau : il faut maintenant se décider. Étant donné que la modélisation des pentaminos suggère un tableau sous la forme d'un tableau à une dimension, prenons cette direction, quitte à changer d'avis plus tard si elle n'est pas suffisamment pratique ; le tableau ci-dessous décrit toutes ces positions :
0 1 2 3 4 5 6 7 8 9
10 11 12 13 14 15 16 17 18 19
20 21 22 23 24 25 26 27 28 29
30 31 32 33 34 35 36 37 38 39
40 41 42 43 44 45 46 47 48 49
50 51 52 53 54 55 56 57 58 59
  • pour le moment il ne paraît pas judicieux de modifier la méthode Ajoute, qui comporte déjà quelques lignes de code déjà testées. Aussi nous allons plutôt partir sur une méthode VerifierPlaceLibre, qui sera testée comme suit :

    Assert.That(plateau.VerifierPlaceLibre(I, 1), Is.True);
Nous obtenons facilement barre rouge puis verte en retournant la constante false, puis true, et il faut alors trianguler avec un deuxième test :

	plateau.Ajoute(I, 1);
	Assert.That(plateau.VerifierPlaceLibre(X, 1), Is.False);
Alors nous nous rendons compte qu'il faut maintenant modifier Ajoute qui a besoin d'un deuxième paramètre, ce qui implique de modifier les trois tests qui portent déjà sur Ajoute.

La barre rouge est alors obtenue, et pour avoir la barre verte, il est indispensable que le plateau mémorise de l'information quand un pentamino est posé. C'est le moment de déclarer un tableau de cases :

private Boolean[] Cases = new Boolean[60];
puis le remplir lors de la pose d'un pentamino

private void Pose(Pentamino pentamino, int position)
{
	VariantesDejaAjoutees[pentamino.Variante] = true;
	TotalVariantesDejaAjoutees++;

	Cases[position] = true;
	foreach (int decalage in pentamino.Decalage)
	{
		Cases[position + decalage] = true;
	}
}
info Noter que Pose reste privée, car aucun test ne porte directement sur elle.
Il est alors possible d'écrire la vérification de place libre :

public Boolean VerifierPlaceLibre(Pentamino pentamino, int position)
{
	if (Cases[position])
	{
		return false;
	}
	else
	{
		Boolean toutes_libres = true;
		foreach (int decalage in pentamino.Decalage)
		{
			toutes_libres = Cases[position + decalage];
			if (!toutes_libres)
			{
				break;
			}
		}
		return toutes_libres;
	}
}
Toutefois, le premier test sur VerifierPlaceLibre ne passe pas, alors que le deuxième passe ! Après examen du code, il y erreur sur le calcul de toutes_libres, qui doit être

toutes_libres = !Cases[position + decalage];
Ici nous voyons l'intérêt d'avoir triangulé ce test, et nous voyons aussi l'intérêt des tests unitaires, qui capturent au plus tôt ce type d'erreur de programmation très fréquente.

La barre verte est alors obtenue. En examinant les possibles duplications, nous pouvons remarquer que deux fois nous avons une gestion de Cases[position] suivie d'un foreach sur les décalages, ce qui introduit un risque car il faut toujours penser à ces deux situations. Si l'on introduit artificiellement un décalage supplémentaire de zéro dans le tableau décrivant les décalages, ce problème disparaîtrait. Modifions donc la classe Pentamino comme suit :

        public int[] Decalage = new int[5];
        public int Variante;

        public Pentamino(int variante, int decalage1, int decalage2, int decalage3, int decalage4)
        {
            Variante = variante;
            Decalage[0] = decalage1;
            Decalage[1] = decalage2;
            Decalage[2] = decalage3;
            Decalage[3] = decalage4;
            Decalage[4] = 0;
        }
Le décalage de zéro est introduit en dernier afin de ne pas casser les tests existants. Au passage on peut noter le grand intérêt de foreach qui nous évite bien des risques de duplications de taille de tableau qu'il faudrait sinon gérer avec des constantes.

Après avoir vérifié que tous les tests passent encore, nous pouvons modifier Pose et VerifierPlaceLibre :

public Boolean VerifierPlaceLibre(Pentamino pentamino, int position)
{
	Boolean toutes_libres = true;
	foreach (int decalage in pentamino.Decalage)
	{
		toutes_libres = !Cases[position + decalage];
		if (!toutes_libres)
		{
			break;
		}
	}
	return toutes_libres;
}
Nous avons donc supprimé une duplication, et simplifié deux fonctions. Les tests passent toujours après cette opération, donc nous avons une certaine confiance dans nos travaux. En raffinant encore un peu il est possible d'éliminer toutes_libres ci-dessus :

public Boolean VerifierPlaceLibre(Pentamino pentamino, int position)
{
	foreach (int decalage in pentamino.Decalage)
	{
		if (Cases[position + decalage])
		{
			return false;
		}
	}
	return true;
}
Ce qui est finalement plus lisible. Enfin, il serait aussi plus lisible de passer Decalage au pluriel (pentamino.Decalages), que Visual Studio peut remplacer facilement à travers toute la solution avec l'outil Refactor/Rename :

Encore une fois, même si ce renommage est trivial, disposer de tests unitaires nous permet de remanier le code en toute sécurité. Ceci est d'autant plus vital que les environnements de développement proposent des outils de remaniement de plus en plus perfectionnés (le Rename ci-dessus en étant l'illustration la plus basique). Pratiquer le TDD est donc un moyen de mieux tirer parti de ces nouveaux outils.

Écrivons encore un test sur VerifierPlaceLibre :

	Assert.That(plateau.VerifierPlaceLibre(I, 9), Is.False);
En effet le pentamino I horizontal ne doit pas pouvoir être posé à la fin de la première ligne, car il déborderait du plateau. Nous obtenons logiquement la barre rouge, car aucune précaution n'a été prise pour l'instant contre cette situation.

Pour arriver rapidement à la barre verte, nous pouvons ajouter artificiellement des cases au tableau, afin de bloquer les possibilités de débordement (ces cases sont souvent appelées des sentinelles) :

(les pentaminos étant toujours décrits par leur position la plus "haute", il n'y pas de débordement possible par le haut du tableau)

Nous avons alors un tableau de 84 cases qu'il faut initialiser soigneusement dans un constructeur de la classe Plateau :

public Plateau()
{
	int position = 0;
	for (int index_ligne = 1; index_ligne <= 6; index_ligne++)
	{
		Cases[position++] = true;
		for (int index_colonne = 1; index_colonne <= 10; index_colonne++)
		{
			Cases[position++] = false;
		}
		Cases[position++] = true;
	}
	for (int index_colonne = 0; index_colonne <= 11; index_colonne++)
	{
		Cases[position++] = true;
	}
}
Cela suffit effectivement à faire passer le test. La suppression des duplications nous impose de factoriser les dimensions du tableau (6, 10, 11) - que nous faisons avec des constantes dans la classe Plateau.

Le constructeur de Plateau est assez complexe, avec plusieurs boucles, et des erreurs possibles sur leurs bornes. Il est donc important d'ajouter des tests complémentaires.

Par exemple le X ne doit pas pouvoir être mis en 37 ni 65, mais il doit pouvoir être mis en 45. J'ajoute donc ces trois tests, chacun avec sa méthode (plutôt que de mettre trois assertions dans une seule méthode). Ces trois tests passent, ce qui laisse penser que le tableau est initialisé correctement.


V-C. Utilisation d'une solution connue

Il serait maintenant agréable de remplir le tableau avec une solution connue, afin d'exercer tout le code déjà écrit sur un test conséquent. Il serait également pratique de pouvoir ajouter les pentaminos sans calculer à la main leur position exacte, par exemple avec une méthode ProchainePositionLibre, qui pourrait être testée comme suit :

public