De la difficulté de faire un bon test

Comment tester qu’un tri a bien été réalisé ? Imaginez que vous deviez tester unitairement la fonction Sort générique de .NET. Comment réaliser le code le plus concis possible, sans redondance mais sans laisser passer de cas particulier, et qui valide que cette fonction réalisera correctement le travail.

Un bon test est toujours plus complexe à réaliser qu’une bonne fonction. Pour cet exemple du tri, voici ce que je propose, suite à une longue discussion avec plusieurs collègues lors d’une formation.

using System;
using System.Collections.Generic;
using System.Text;
 
using NUnit.Framework;
 
namespace Tests
{
    [TestFixture]
    public class Class1
    {
        private List<Personne> Liste = new List<Personne>();
 
        [SetUp]
        public void Initialisation()
        {
            Liste.Clear();
            Liste.Add(new Personne("Gouigoux", "JP", 35));
            Liste.Add(new Personne("Lagaffe", "Gaston", 39));
            // etc...
        }
 
        [Test]
        public void TestTri()
        {
            List<Personne> Copie = new List<Personne>(Liste);
 
            Liste.Sort(ComparateurAgeDescendant);
 
            // PHASE I
            Assert.AreEqual(Liste.Count, Copie.Count);
 
            // PHASE II
            Assert.IsTrue(Copie.TrueForAll(delegate(Personne p)
            {
                return Liste.Contains(p);
            }));
 
            // PHASE III
            for (int Index = 0; Index < Liste.Count - 1; Index++)
                      Assert.IsTrue(Liste[Index].Age >= Liste[Index + 1].Age);
 
            // PHASE IV
            for (int Index = 0; Index < Liste.Count - 1; Index++)
                if (Liste[Index].Age == Liste[Index + 1].Age)
                {
                    int PosA = Copie.IndexOf(Liste[Index]);
                    int PosB = Copie.IndexOf(Liste[Index + 1]);
                    Assert.Less(PosA, PosB);
                }
        }
 
        private int ComparateurAgeDescendant(Personne a, Personne b)
        {
            return b.Age.CompareTo(a.Age);
        }
    }
}

La première assertion doit consister en une vérification que la longueur de la liste après le tri doit être la même qu’avant le tri.

La seconde doit valider que chaque entité contenue dans la liste avant traitement doit être retrouvée dans la nouvelle liste.

La troisième (celle que tout le monde fera, sans parfois réaliser que c’est loin d’être suffisant) valide que l’ordre est bien celui qu’on souhaitait, en vérifiant que chaque entité est bien dans l’ordre correspondant à sa position par rapport à celle qui suit.

Enfin, la quatrième est celle que tout le monde ou presque oublie : pour qu’un tri soit réputé stable (mais il est vrai que je n’avais pas précisé cette exigence au début du post), il faut que deux objets qui sont égaux pour la comparaison, mais pourtant bien distinct, restent dans le même ordre. Dans notre exemple, si j’avais eu le même âge que Gaston Lagaffe (en plus d’avoir le même pull et le même tact), le tri devrait laisser la liste triée avec nos deux entités dans leur ordre initial.

[Edit]

Je reprends ci-dessous un commentaire de Guillaume Collic et ma réponse (désolé, je ne migre que les commentaires les plus importants en migrant ce blog depuis l’ancienne adresse) :

Le commentaire initiale de Guillaume :

J’approuve ce que tu tests (le fond), mais par contre la structure m’étonnes (la forme). Ce serait plus clair à mon avis de séparer chaque phase dans son propre test avec un nom plus parlant que “Phase II”. Cela impliquerait aussi que le test sur l’ordre devrait pouvoir passer même si le compte n’est pas bon par exemple. J’ai l’impression que la source d’une anomalie serait identifiée plus rapidement, en plus d’une meilleure lisibilité.

Ma réponse :

C’est vrai que si on veut rester dans l’esprit test unitaire, on devrait avoir une fonction par phase.

Je fais avec une seule fonction, parce que les Assert n’ont de sens que les uns avec les autres. Par exemple, la phase IV pourrait très bien renvoyer un faux positif si la phase I n’était pas vérifiée. Du coup, j’ai plus tendance à voir le tout comme un seul test unitaire “fonctionnement du tri”, avec plusieurs phases d’Assert, plutôt que plusieurs tests unitaires, car ils ne seraient alors pas indépendants.

About JP Gouigoux

Jean-Philippe Gouigoux est Architecte Logiciel, MVP Connected Systems Developer. Il intervient régulièrement à l'Université de Bretagne Sud ainsi qu'à l'Agile Tour. Plus de détails sur la page "Curriculum Vitae" de ce blog.
This entry was posted in .NET, ALM, C#, Tests and tagged . Bookmark the permalink.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha Captcha Reload