IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

BROUILLON - EN COURS - DÉVELOPPEMENT DIRIGÉ PAR LES TESTS et principes de conservation

Date de publication : 14 août 2008

Par Bruno Orsier (Site Web)
 

TODO

I. Un résultat de calcul doit rester constant
II. Une caractéristique doit être conservée


I. Un résultat de calcul doit rester constant

L'idée de ce principe me vient d'une application scientifique co-développée il y a quelques années avec mon camarade Emmanuel Etasse. En gros l'application permettait d'ouvrir un fichier, de régler quelques paramètres, et d'appuyer sur un bouton qui lançait des calculs, et enfin des graphiques et des tables étaient mis à jour. C'est très représentatif de notre travail habituel. Dans ce cas de cette application précise, après quelque temps d'utilisation, un utilisateur s'est aperçu que chaque fois qu'il appuyait sur le bouton "Calculer" il obtenait des résultats différents ! Le résultat était juste la première fois mais faux les fois suivantes. Nous n'avions pas détecté le problème nous-mêmes dans nos tests car il ne nous était jamais venu à l'esprit d'appuyer plusieurs fois de suite sur le bouton.

Evidemment une fois identifié, le problème n'était pas trop difficile à comprendre : il était causé par une erreur d'initialisation cachée dans quelques milliers de lignes de code. Mais j'en garde un mauvais souvenir car nous avions alors perdu pas mal de crédit auprès de nos utilisateurs et managers, malgré nos tests.

En fait à l'époque nous dépendions entièrement de tests manuels. Heureusement depuis nous avons beaucoup progressé en matière de développement dirigé par les tests et autres tests automatisés. Toutefois, si l'on n'y prend pas garde, rien n'empêche de faire exactement la même erreur en pratiquant le développement dirigé par les tests ! Le reste de cette section illustre concrètement ce risque, et comment y remédier simplement.

En simplifiant à l'extrême nos classes de calcul de l'époque, on arrive à quelque chose comme :

    public class ClasseQuiImplementeUnCalculComplexe
    {
        private int etat_initial;

        public ClasseQuiImplementeUnCalculComplexe (int valeur)
        {
            etat_initial = valeur;
        }

        public int Calcule(int parametre)
        {
            int resultat = etat_initial + parametre;
            etat_initial = parametre; // ERREUR ICI
            return resultat;
        }
    }
Il y a de grandes chances pour que vos tests unitaires ressemblent à celui-ci (généré automatiquement grâce au "Wizard" de Visual Studio) :

        [TestMethod()]
        public void CalculeTest()
        {
            int etat_initial = 100; 
            ClasseQuiImplementeUnCalculComplexe target = new ClasseQuiImplementeUnCalculComplexe(etat_initial); 
            int parametre = 10; 
            int valeur_attendue = 110; 
            int valeur_calculee = target.Calcule(parametre);
            Assert.AreEqual(valeur_attendue , valeur_calculee);
        }
Et malheureusement ce genre de test ne capture pas le problème d'initialisation sournoisement introduit dans les lignes de code ci-dessus (des milliers dans la réalité). En fait il nous faut un test du type :

        [TestMethod()]
        public void CalculeDoitToujoursDonnerLeMemeResultat()
        {
            int etat_initial = 100;
            ClasseQuiImplementeUnCalculComplexe target = new ClasseQuiImplementeUnCalculComplexe(etat_initial);
            int parametre = 50;
            Assert.AreEqual(target.Calcule(parametre), target.Calcule(parametre)); 
        }
warning Dans ce deuxième test j'ai volontairement évité de préciser la valeur attendue, afin d'attirer votre attention sur le fait que l'on peut programmer des tests très pertinents sans même connaître précisément la valeur attendue. Cela me paraît très important car dans la vie réelle il n'est pas toujours simple de spécifier précisément une valeur attendue - il est alors très agréable de pouvoir réaliser des tests indirects comme celui ci-dessus.

II. Une caractéristique doit être conservée

Cet exemple provient d'une autre application scientifique, dans laquelle un objet métier complexe devait subir une transformation particulière qui produisait deux nouveaux objets remplaçant le premier. L'algorithme de la transformation était relativement complexe et il n'était pas possible de prédire très précisément les nouvelles caractéristiques des objets obtenus - dans le sens où l'on pouvait facilement les déterminer à 10-2 près mais pas à 10-8 près. Le test était manuel, et consistait à vérifier que des valeurs calculées correspondaient aux valeurs attendues à 10-2.

Pour illustrer notre propos, nous pouvons considérer que l'objet métier est un Rectangle, que l'on s'intéresse à sa Surface, et que la transformation consiste à le Partager en deux.

    public class Rectangle
    {
        private readonly double largeur;
        private readonly double longueur ;

        public Rectangle (double largeur, double longueur)
        {
            this.longueur = longueur;
            this.largeur = largeur;
        }

        public double Surface()
        {
            return longueur*largeur;
        }

        public List<Rectangle> Partage()
        {           
            List <Rectangle> resultat = new List<Rectangle>();

            // le calcul du milieu est intentionnellement faux
            int milieu = (int) longueur/2;

            resultat.Add(new Rectangle(largeur,  milieu));
            resultat.Add(new Rectangle(largeur, milieu));
            return resultat;
        }
Dans notre cas réel il y avait une erreur subtile dans la transformation (elle est modélisée ci-dessus par le calcul du milieu qui est faux car réalisé avec une division entière). Elle est restée indétectée jusqu'à ce que quelqu'un ait l'idée de appliquer cette transformation plusieurs fois de suite et constate alors une variation inattendue sur une caractéristique globale. En effet la surface totale de tous les objets se mettait à diminuer, alors qu'elle aurait dû rester strictement constante. Nous n'avions pas pensé à exploiter le fait que cette surface était invariante dans cette transformation, ce qui aurait été bien plus simple à vérifier manuellement que de vérifier des valeurs particulières à une certaine précision.

L'automatisation des tests, si l'on ne fait pas explicitement attention à exploiter une telle caractéristique invariante, peut tout à fait rencontrer ce genre de problème.

En effet, on risque de se limiter à des méthodes de test comme celle ci-dessous, qui est l'équivalent de notre test manuel initial dans lequel on se contentait d'une faible précision car on ne connaissait pas exactement la valeur attendue :

        [TestMethod()]
        public void PartageTest()
        {
            const double FAIBLE_PRECISION = 1E-2;
            double largeur = 10; 
            double longueur = 20.05;
            double surface_attendue = 100;

            Rectangle target = new Rectangle(largeur, longueur); 
            List<Rectangle> rectangles =  target.Partage();
            Assert.AreEqual(surface_attendue, rectangles[0].Surface(), FAIBLE_PRECISION);
            Assert.AreEqual(surface_attendue, rectangles[1].Surface(), FAIBLE_PRECISION);
        }
Ce test réussit malgré l'erreur introduite dans la méthode Partage ! Par contre si l'on prend en compte l'invariance de la surface totale, on peut exiger une grande précision dans le test, car même sans connaître la valeur attendue on sait qu'elle doit être exactement la même que la valeur initiale :

        [TestMethod()]
        public void PartageDoitConserverLaSurfaceTotale()
        {
            const double HAUTE_PRECISION = 1E-8;
            double largeur = 10;
            double longueur = 20.05;
            
            Rectangle target = new Rectangle(largeur, longueur);
            List<Rectangle> rectangles = target.Partage();

            Assert.AreEqual(target.Surface(), rectangles[0].Surface() + rectangles[1].Surface(), HAUTE_PRECISION);
        }
Et à ce moment-là le test échoue et capture bien l'erreur dans Partage.

warning Remarquez à nouveau qu'il n'y a pas de valeur attendue dans ce dernier test.


Valid XHTML 1.1!Valid CSS!

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2008 Bruno Orsier. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.