JUnit: Résumé du chapitre 3

Cet article est un résumé du chapitre 3 du livre JUnit, mise en oeuvre pour automatiser les tests en Java, écrit par Benoît Ganthaume et publié aux éditions ENI.

                              __________________________________

Classiquement, l’analyse d’un projet s’effectue selon le triptyque de variables interdépendantes : coût, qualité, périmètre.

Les tests automatiques font circuler la connaissance entre les acteurs, leur permettent de garder une visibilité sur l’évolution du projet en validant la cohérence et la testabilité de l’architecture tout en objectivant les exigences. Ils fournissent un indicateur de progression et assurent contre les régressions du code. En fonction de la méthodologie de travail, les quatre activités fondamentales (recueil des besoins, conception, codage et tests) prennent des places et finalités différentes :

Les méthodes linéaires :

Le modèle de la cascade: Chaque erreur aura un coût de correction plus élevé lors de l’étape suivante, imposant ainsi la validation totale d’une étape avant de la franchir. Les tests arrivent uniquement en fin de chaîne, doivent couvrir totalement le code et idéalement être effectué par des personnes qui ne l’ont pas écrit. Les phases préliminaires du développement (conception, documentation, prototype), ainsi que l’implication du client sont fondamentales.

Le cycle en V : Conçu sur le modèle de la cascade, il pallie au rôle tardif des tests dans ce dernier : Chaque étape de la cascade doit être validée par un type de test particulier : Les tests unitaires sont l’affaire des développeurs, les tests d’intégration (validant le fonctionnement temporel ou architectural de l’application après mise en commun des différentes parties) garantis par les architectes. La Maîtrise d’oeuvre (MOE) livre les tests de validation fonctionnels (coté client) et de performance, la Maîtrise d’ouvrage (MOA) s’occupe des tests de recette, vérifiant le comportement de l’application dans des conditions réelles.

Les méthodes itératives et incrémentales: Approches agiles

La spirale, pilotée par les risques, est la première formalisation agile. Elle se découpe en quatre phases validant un incrément, pour conduire à l’itération suivante. L’évaluation initiale classique n’est plus applicable, l’identification des risques devient primordiale.

Le “Rational Unified Process” (RUP) fait intervenir tous les types d’activités au cours de phases (inception, élaboration, construction, transition) destinées à réduire progressivement les risques et se découpant en itérations. Le RUP définit différents rôles, dont celui de testeur, qui évolue au cours des différentes phases. Les tests automatiques, bien qu’optionnels, apportent de précieux atouts à chaque rôle.

La méthode SCRUM s’appuie sur des itérations de durées fixes (sprints) destinées à faire progresser le logiciel par ajouts successifs, mais ne prévois pas de méthodes de test.

L’eXtreme programming (XP) intègre gestion de projet et pratiques d’ingénierie, systématisées et poussées à l’extrême, permettant d’assurer une faible évolution du coût de changement. L’XP s’appuie sur le code pour gérer l’ensemble du projet (documentation et tests compris). Généralement, chaque itération (devant aboutir sur la livraison d’un incrément fonctionnel) se décompose en conception, test puis développement. Le code actif est donc écrit pour faire passer un test précis. La conception se doit d’être simple, les tests automatiques et leurs intégration continue, assurant une protection contre la régression. Les tests de recette constituent un contrat client à remplir. L’équipe, composée de différents rôles, communicant via un vocabulaire commun, est collectivement responsables du code et des tests. Les tests occupent une place fondamentale en XP, leur nombre et leur fréquence apportent une indication sur la charge et le rythme de travail de l’équipe.         

JUnit: Résumé du chapitre 2

Cet article est un résumé du chapitre 2 du livre JUnit, mise en oeuvre pour automatiser les tests en Java, écrit par Benoît Ganthaume et publié aux éditions ENI.

                              

Le point de départ de la création d’un logiciel est l’identification du besoin client (comprendre ici l’utilisateur et non l’acheteur), le résultat final est le produit. Les 5 phases du développement comportant inévitablement des erreurs émanant des différents acteurs impliqués, le test répond à un double objectif : ne pas les reproduire et valider chaque étape de production. Il donne un feedback sur la qualité via une boucle de rétroaction.

Le concept de qualité est influencé par notre perception, ses critères varient en fonction du point de vue de chaque acteur (client, direction, équipe de développement). La notion de logiciel « juste assez bon » implique des compromis entre les parties du projet, ainsi qu’une gestion des risques orientée client : les défauts l’impactant fortement sont à corriger en priorité.

Les tests manuels nécessitent une personne pour exécuter et contrôler le comportement de l’application. Leur suivi permet d’établir des indicateurs de qualités. Leurs avantages sont la mise en situation réelle, la validation in situ, le faible coût de mise en place et la flexibilité inhérente au facteur humain. A contrario, certaines erreurs sont difficiles à valider, leurs exécutions sont couteuses et les résultats nécessitent du temps. Enfin, le contrôle humain engendre des risques d’erreurs.

Les tests automatiques sont gérés par un programme qui les exécute automatiquement ou sur demande. L’automatisation relève de l’intégration continue, et génère des rapports de tests automatiques.  Couteux à mettre en place, moins souples que les tests manuels, leurs coûts et délais d’exécution restent faibles, constants et réguliers, facilitant leurs réexécution.

Le choix entre ces types de tests implique un équilibre en fonction du contexte de l’application et de sa complexité.

L’automatisation accentue la vélocité du développement, l’équipe garde une vision continue de l’impact de son travail. Le retour sur investissement est élevé, les couts et les temps de correction sont réduits. C’est un facteur clef de compétitivité s’ils sont entretenus et améliorés en permanence.

Les tests automatiques doivent êtres rapides, indépendants les uns des autres, répétables, auto-validant, et écrit avant le code.

Intégration de JUnit dans Maven

Cet article est un extrait du livre sur JUnit

Présentation de l’outil

Maven est également très répandu. C’est un outil de compilation puissant intégrant de nombreux plug-ins d’extension et permettant, entre autres, de gérer les dépendances avec une grande facilité. Maven n’est pas un IDE dans le sens ou il laisse le soin au développeur de choisir son éditeur de code préféré. Les projets Maven sont d’ailleurs faciles à importer dans la plupart des IDE. C’est aujourd’hui un standard pour de nombreux projets. Il dédie à JUnit une phase complète lors du processus de compilation. Ainsi, la compilation rapporte une erreur si les tests ne passent pas. Par ailleurs, il suggère fortement l’organisation des fichiers sources suivant un schéma standard :

src
+ main
|  + java
|  |  + com
|  |  |  + monprojet
|  |  |  |  + Classe.java
+ test
|  + java
|  |  + com
|  |  |  + monprojet
|  |  |  |  + ClasseTest.java

Guide pas à pas

Maven est un outil en mode console.

Commencez par ouvrir une console de terminal.

Tapez la commande suivante :

mvn archetype:create -DgroupId=exemple -DartifactId=calculatrice -Dversion=1.0

Maven va créer un dossier calculatrice qui contient un fichier pom.xml.
Éditez ce fichier.

Vous remarquez qu’il est au format XML et qu’il contient la section suivante :

<dependencies>
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>3.8.1</version>
    <scope>test</scope>
  </dependency>
</dependencies>

Celle-ci indique que le projet a besoin de JUnit.

Changez simplement le numéro de version pour 4.7.

Ajoutez également la section suivante pour indiquer que nous souhaitons compiler en Java 1.5 :

<build>
  <finalName>Calculatrice</finalName>
  <plugins>
     <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.5</source>
          <target>1.5</target>
        </configuration>
      </plugin>
  </plugins>
</build>

Créez le répertoire src/main/java/math.

Sauvegardez-y le fichier Addition.java.

package math;

class Addition {
    public Long calculer(Long a, Long b) {
        return a+b;
    }
    public Character lireSymbole() {
        return '-';
    }
}

Créez le répertoire src/test/java/math.

Sauvegardez-y le fichier AdditionTest.java.

package math;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;

public class AdditionTest {
    protected Addition op;

    @Before
    public void setUp() {
        op = new Addition();
    }

    @After
    public void tearDown() {
    }

    @Test
    public void testCalculer() throws Exception {
        assertEquals(new Long(4), op.calculer(new Long(1),
                                              new
Long(3)));
    }

    @Test
    public void testLireSymbole() throws Exception {
        assertEquals((Character)'+', op.lireSymbole());
    }
}

Enfin, lancez la commande suivante :

# mvn test
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running math.AdditionTest
Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 
0.03 sec <<< FAILURE!

Results :

Failed tests: 
  testLireSymbole(math.AdditionTest)

Tests run: 2, Failures: 1, Errors: 0, Skipped: 0

Comme vous pouvez le constater, Maven produit un rapport de tests en mode textuel avec la synthèse du nombre de tests et la liste des tests en échec. Par défaut Maven produit les rapports détaillés dans le dossier target/surefire-reports.

Cet article est un extrait du livre sur JUnit

Définition de la couverture de code illustrée avec JUnit

Cet article est un extrait du livre sur JUnit

La couverture de code est une mesure qui permet d’identifier la proportion du code testé. Les outils de couverture permettent de réaliser cette mesure, d’identifier les parties de code non couvertes et de les visualiser.

Cette mesure peut être calculée de différentes manières. Pour illustrer ces différents modes de calculs, nous utiliserons le code suivant :

public class CompteBancaire {
  protected String numeroDeCompte;

  public CompteBancaire(String numeroDeCompte) {
   this.numeroDeCompte = numeroDeCompte;
  }

  public String interrogerCompte(String motdepasse) {
    String resultat = null;
    if (motdepasse.equals("madatedenaissance")) {
      Integer solde = calculDuSolde();
      resultat = "Information du compte:";
      resultat += numeroDeCompte;
      resultat += ".\r\n";
      resultat += "Votre compte est créditeur de ";
      resultat += solde.toString();
      resultat += "€";
    } else {
      gererErreurDeMotDePasse();
    }
    return resultat;
  }

  protected Integer calculDuSolde() {
    Integer solde = new Integer(0);
    solde = 1000;
    solde += 100;
    solde -= 200;
    solde += 400;
    solde -= 300;
    return solde;
  }
  protected void gererErreurDeMotDePasse() {
    throw new RuntimeException("Erreur non gérée correctement");
  }
}

Nous testerons cette classe à l’aide du test suivant :

public class CompteBancaireTest {
  @Test
  public void testInterrogerCompte() {
    CompteBancaire compte = new CompteBancaire("0123456789");
    String verif = "Information du compte:0123456789.\r\n";
    verifi += "Votre compte est créditeur de 1000€";
    assertEquals(
          verif,
          compte.interrogerCompte("madatedenaissance"));
  }
}

La classe testée contient 20 lignes de code exécutables, 4 méthodes et une condition, soit 2 chemins logiques possibles.

Nous pouvons alors calculer différentes métriques de couverture.

Couverture linéaire

La couverture en lignes de code indique la proportion des lignes de code exercées lors du test. Elle est la plus répandue et la plus simple à visualiser : chaque ligne compte pour un élément. Dans cet exemple, 18 lignes sur 20 sont couvertes. La couverture en lignes sera donc de 18/20=90 %.

La couverture en méthodes calcule la proportion des méthodes couvertes au moins une fois. Chaque méthode compte pour un. Ainsi dans notre exemple, sur les 4 méthodes présentes, 3 sont testées. La couverture sera donc de 75 %. Cette mesure ressemble à la précédente avec une granularité plus large.

Couverture conditionnelle

La couverture conditionnelle calcule la proportion des chemins logiques couverts. C’est la mesure la plus compliquée à appréhender car elle change notre vision du code : au lieu d’en avoir une vision linéaire sous forme de listing, elle pousse vers une vision arborescente du code. Chaque condition compte pour deux. Pour couvrir la condition à 100 %, il faut tester le cas où la condition est vraie puis celui où elle est fausse. Ainsi dans le cas d’une structure conditionnelle if/then/else, couvrir chaque branche compte pour un. Dans notre exemple, seul le cas then est couvert. La couverture de la classe est donc de 50 %.

Quelle mesure utiliser ?

La couverture en lignes de code est intéressante car très simple à appréhender. De plus, elle permet de facilement mettre en évidence les portions de code non testées par blocs.

Dans notre exemple, on peut constater que le cas d’erreur n’est pas du tout testé.

Cependant, il est facile d’être leurré par les chiffres remontés : en effet, les cas d’exécution normale (quand tout va bien) sont souvent ceux qui ont le plus de poids. Les cas d’erreurs sont en général bien plus légers. Ainsi, un code couvert à 90 % peut donner un faux sentiment de sécurité lorsque seul le cas nominal est testé.

C’est ce qu’illustre notre exemple : la gestion du cas d’erreur lève une exception système.

Il est même possible d’être complètement floué par cette mesure. Considérons le code suivant :

public class Secret {
  public String donnerSecret(String motdepasse) {
    String secret = null;
    if (motdepasse.equals("aliens")) {
      secret = " La vérité est ailleurs ";
    }
    return secret.trim();
  }
}
Testons cette classe à l'aide du test suivant :

public class SecretTest {
  @Test
  public void testDonnerSecret () {
    Secret message = new Secret();
    String verification = "La vérité est ailleurs";
    assertEquals(verification,
    message.donnerSecret("aliens"));
  }
}

Ce test couvre à 100 % le code autant en ligne de code qu’en procédure.

Pourtant, un bug critique se cache manifestement dans le code… En effet, l’exécution du programme suivant fait planter l’application :

public class ApplicationConsole {
  public static void main(String[] args) {
    Secret messager = new Secret();
    String message = messager.donnerSecret("motdepasseoublie");
    System.out.println(message);
  }
}
Exception in thread "main" java.lang.NullPointerException
              at Secret.donnerSecret(Secret.java:8)
              at ApplicationConsole.main(ApplicationConsole.java:4)

Ainsi, la couverture en ligne de code ne suffit pas pour donner une vision satisfaisante de la qualité intrinsèque du code.

La couverture conditionnelle est intéressante car elle donne une autre vision du code, en chemins d’exécutions possibles. Elle pondère différemment les proportions testées en partant du principe que chaque chemin a le même poids, indépendamment de sa longueur. Cependant, cette mesure ne permet pas d’apprécier la proportion de code testée. Ainsi, un code ne contenant que peu de chemins logiques, tous couverts, peut donner une fausse impression de sécurité. C’est pourquoi il faut balancer cette mesure avec une mesure en lignes de code ou en méthodes.

Ainsi, la mesure de la couverture de code sera une combinaison de différents modes de calcul. Chaque outil a son propre mode de calcul offrant le choix aux développeurs.

Cet article est un extrait du livre sur JUnit

Utilisation des bouchons pour automatiser ses tests

Cet article est un extrait du livre sur JUnit

Qu’est-ce qu’un bouchon ?

Lors de sa conception, un système logiciel est décomposé en modules ou sous-parties ce qui permet de simplifier le travail à faire. Lors de la fabrication de ces modules, les développeurs les valideront à l’aide de tests unitaires. Cependant, le module en cours de fabrication sera très souvent dépendant d’autres modules.

Dans le schéma ci-dessus, le module III :

  • dépend en amont des entrées fournies par les modules I et II ;
  • doit alimenter en aval les modules IV et V.

Ces dépendances peuvent compliquer l’écriture de tests automatiques, voire la rendre impossible dans certains cas. Par ailleurs, ces dépendances rendent les tests vulnérables aux évolutions des modules aussi bien en amont qu’en aval. Ces contraintes risquent d’éroder la motivation des développeurs au point de mettre en péril l’écriture des tests automatiques. Dans ces cas, l’utilisation de bouchons permet de passer outre ces limitations.

Un bouchon est un élément qui peut se substituer à un autre afin d’en imiter le comportement. Cette imitation peut être plus ou moins sophistiquée. On distingue notamment différents types de bouchons.

  • Les bouchons statiques: ils sont très simples et simulent le fonctionnement d’une classe de façon passive. Typiquement, l’appel d’une méthode retournera toujours la même valeur, quels que soient les paramètres.
  • Les bouchons dynamiques: ils intègrent une intelligence simple. Ils sont typiquement capables de prendre en compte des paramètres afin d’adapter leur réponse.
  • Les bouchons intelligents: ils simulent le fonctionnement de l’élément remplacé. Ils prennent en compte les paramètres d’entrée et adaptent leur réponse comme le ferait la classe d’origine.

Selon l’objet testé, il sera plus approprié d’utiliser de nombreux bouchons statiques peu complexes qui simuleront chacun un comportement précis ou un seul bouchon intelligent qui simulera le sous-système complet, mais qui sera complexe à développer.

Quand utiliser les bouchons ?

Comme nous l’avons évoqué, l’utilisation des bouchons est particulièrement indiquée lorsque l’écriture des tests unitaires sur une classe devient compliquée. En particulier lorsque :

  • Le sous-système est lent : lorsque le sous-système ralentit l’exécution des tests unitaires, il peut être avantageux de le remplacer par un bouchon intelligent qui permet d’accélérer les tests. Il peut s’agir par exemple d’un système distant ou ayant une capacité de traitement limitée.
  • Le sous-système fonctionne de façon non déterministe : c’est typiquement l’exemple de fonctions ayant une notion de temps ou utilisant des marquages temporels : par exemple une classe de journalisation.
  • Le sous-système utilise une interface utilisateur bloquante : c’est par exemple le cas d’un module qui a besoin de saisies de la part de l’utilisateur.
  • L’objet réel n’existe pas encore : un cas plus fréquent qu’il n’y paraît a priori. Même si l’implémentation est triviale, il peut être difficile d’accepter de mobiliser de l’énergie pour construire un bouchon au lieu de fabriquer le module réel. Pourtant bien souvent le gain final est important. Par exemple dans une démarche d’écriture des tests préalables à l’implémentation, cela peut avoir un sens si les tests et le code sont développés par des équipes différentes : le bouchon permettra de valider la campagne de tests en attendant le code réel.
  • Le sous-système est difficile à initialiser : lorsque le sous-système nécessite une initialisation complexe, il peut être avantageux de passer par un bouchon. C’est typiquement le cas de tests de montées en charge qui nécessitent l’utilisation de comptes utilisateurs prédéfinis : si le test utilise plusieurs milliers de comptes, la création de ces comptes peut devenir problématique.
  • Les mises en erreur ne sont pas reproductibles : en effet, les cas d’erreur d’un sous-système peuvent être difficiles à reproduire, rendant ainsi le test du système impossible à contrôler. C’est typiquement le cas pour la disponibilité d’un serveur Web ou d’une base de données. Dans ces cas, des bouchons statiques produisant systématiquement l’erreur voulue seront très adaptés.
  • Les effets réels ne sont pas souhaités : c’est typiquement le cas d’une API bancaire. Dans ce cas il est souhaitable de tester la procédure de paiement sans débiter réellement la carte bleue.

D’une manière générale, l’utilisation des bouchons permet de simplifier l’écriture des tests unitaires. Cependant, l’effort d’écriture de ces derniers peut devenir important, en particulier dans le cas de bouchons intelligents. Il faut donc toujours être vigilant à ce que cet investissement soit justifié. En particulier, un bouchon utilisant un autre bouchon doit être exceptionnel et interprété comme le signe d’une complexification qui devrait alerter les développeurs.

Afin d’utiliser un bouchon, il faut distinguer leur fabrication à proprement parler de la possibilité de les mettre en œuvre. Nous commencerons par décrire ce dernier point avant de voir le premier au travers d’un exemple détaillé.

La suite dans le livre sur JUnit

Tutoriel Eclipse – JUnit: Mon premier test automatique

Cet article est un extrait du livre sur JUnit

Démarche

Nous allons voir comment écrire notre premier test unitaire pas à pas. Pour ce faire, nous utiliserons le projet de la calculatrice comme exemple.

  • Tout d’abord, nous créerons le projet de calculatrice.
  • Puis nous écrirons la classe d’addition.
  • Ensuite, il faudra créer le répertoire des tests dans lequel seront classés tous les tests. Chaque équipe peut décider de la façon dont elle souhaite organiser son répertoire de code source et en particulier le code de test. Il existe néanmoins une façon classique de procéder qui consiste à créer un dossier de tests symétrique au répertoire des sources pour y classer les tests selon la même organisation de paquet. Cette façon de procéder a l’avantage d’isoler les classes de tests des classes de source tout en permettant aux tests d’accéder à la portée de définition du paquet. Ainsi, lors de la génération de code, il est simple d’isoler les tests du code tout en gardant les classes de tests proches du code source dans une vue par paquet.
  • Enfin, nous pourrons écrire la classe de tests voulue.

Eclipse

Présentation de l’IDE

Eclipse est aujourd’hui la référence en matière d’environnement de développement et c’est probablement l’outil le plus utilisé. Il bénéficie du soutien de gros industriels ainsi que d’une large communauté. Il intègre aujourd’hui JUnit en standard.

Guide pas à pas

  • Cliquez sur File – New – Java Project.
  • Nommez votre projet calculatrice en laissant les options par défaut puis cliquez sur OK.
  • Ajoutez un package math dans le répertoire src.
  • Ajoutez la classe Addition dans le répertoire src.
package math;

class Addition {
   public Long calculer(Long a, Long b) {
      return a+b;
   }
   public Character lireSymbole() {
      return '-';
   }
}
  • Ajoutez un nouveau dossier de sources nommé tests au même niveau d’arborescence que src.
  • Dans l’explorateur de paquets, faites un clic droit sur la classe Addition.
  • Dans le menu contextuel, cliquez sur New – JUnit Test Case.

Un panneau s’affiche alors :

Dans ce panneau :

  • Sélectionnez le bouton radio New JUnit 4 test.
  • Changez le dossier Source folder pour tests.
  • Nommez la classe AdditionTest.
  • Cochez les cases setUp() et tearDown().
  • Dans le champ Class under test, saisissez math.Addition.
  • Enfin cliquez sur Finish.

Eclipse va remarquer que la bibliothèque de JUnit est absente du projet et vous propose d’ajouter automatiquement cette dernière au projet.

  • Dans le panneau qui apparaît, cliquez sur OK.

Eclipse va maintenant créer automatiquement le squelette de la classe de test :

package math;

import org.junit.After;
import org.junit.Before;

public class AdditionTest {

   @Before
   public void setUp() throws Exception {
   }

   @After
   public void tearDown() throws Exception {
   }

}

Il ne reste plus alors qu’à remplir cette dernière.

package math;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;

public class AdditionTest {
   protected Addition op;

   @Before
   public void setUp() {
      op = new Addition();
   }

   @After
   public void tearDown() {
   }

   @Test
   public void testCalculer() throws Exception {
      assertEquals(new Long(4), 
                   op.calculer(new Long(1), new Long(3)));
   }

   @Test
   public void testLireSymbole() throws Exception {
      assertEquals((Character)'+', op.lireSymbole());
   }
}
  • Dans l’explorateur de paquets, faites un clic droit sur la classe AdditionTest.
  • Dans le menu contextuel, cliquez sur Run As – JUnit test.
  • Enfin, le premier rapport de tests s’affiche !

La barre de progression est rouge, indiquant qu’au moins un test est en échec. Le rapport d’erreur permet de visualiser les tests en échec et d’afficher la cause et l’origine du problème. Dans ce cas, une erreur s’est glissée sur le symbole de l’addition!

Et vous, comment générez-vous vos tests automatiques?

À quoi sert le test ?

Cet article est un extrait du livre sur JUnit

Avant de s’interroger sur la finalité des tests, examinons leur objet : le logiciel. À la base du logiciel fabriqué il y a toujours un besoin client. Cette notion de client est à prendre au sens très large d’utilisateur, qui n’est pas forcément le client au sens commercial. Ce client a besoin de faire quelque chose et le logiciel l’aide à atteindre son but. Le point de départ est donc le besoin client, l’idée, et le point d’arrivée est le logiciel produit.

De l'idée au logiciel.

De l'idée au logiciel.

Dans un monde parfait, tester est une perte de temps.
En effet dans un monde parfait le processus de fabrication d’un logiciel pourrait être décrit ainsi :

Recueillir les besoins ➝ Imaginer ➝ Écrire ➝ Compiler ➝ Exécuter

Dans un monde parfait, l’erreur n’existe pas. Il est donc inutile de tester.

Sauf que nous ne vivons pas dans un monde parfait, et le propre de la vie est de faire des erreurs. Rien de surprenant donc à ce que l’homme en fasse. Sénèque le jeune nous rappelle, il y a déjà longtemps, que l’erreur est humaine, ‘Errare humanum est’. Conscient de cela, l’homme va tester ce qu’il fabrique dans un premier objectif de validation. L’utilité du test est alors de vérifier que ce qui a été fabriqué est conforme à ce qui a été demandé.

Pour être précis, la citation de Sénèque complète retenue par l’histoire est : ‘Errare humanum est perseverare diabolicum’. Si l’erreur est humaine, il est diabolique de persévérer (dans l’erreur). Ainsi, s’il est tolérable de faire des erreurs, il n’est pas acceptable de les reproduire. Les tests prennent alors un nouveau sens qui va au-delà de la validation : ils permettent de s’assurer que les erreurs passées ne seront pas reproduites en vérifiant les points qui ont déjà fait défaut. En matière logicielle, ces tests sont dits de non-régression.

Les erreurs qu’essaient d’éliminer les tests peuvent arriver à n’importe quel moment du processus décrit ci-dessus.

Recueillir les besoins : durant cette phase, le client exprime ses besoins, ses objectifs. Le premier risque est que le client se trompe dans ce qu’il veut. Si cela peut sembler peu probable a priori, la réalité du terrain montre qu’il en est tout autrement. Entre le début et la fin d’un projet, il est rarissime que le client ne change pas sa demande.

Imaginer : cette phase est le propre de l’activité humaine. De toutes les étapes mentionnées ici, c’est la moins automatisable. Les risques d’erreur sont nombreux. Sans être exhaustif, en voici quelques-unes :

  • Mauvaise compréhension du besoin.
  • Élaboration d’une solution valide sur le papier mais impossible à réaliser.
  • Conception d’une solution largement surdimensionnée.

Écrire : cette phase est le prolongement naturel de la précédente. Elle correspond à la structuration de l’idée sous une forme standardisée. Le logiciel peut être écrit directement à l’aide de code ou sous la forme d’une description dans un métalangage. Les erreurs possibles sont encore nombreuses :

  • Mauvaise compréhension de l’idée d’un autre.
  • Difficultés à formaliser ses propres idées.
  • Non respect du standard.

Compiler : cette phase consiste à transformer le code compréhensible par un humain en un langage compréhensible par une machine. Aujourd’hui, cette phase est très automatisée… Par d’autres logiciels écrits selon les mêmes règles, pouvant donc intégrer des erreurs ! Les outils employés étant largement diffusés, le risque devient de plus en plus faible, mais il existe toujours, surtout lorsque le code est soumis à différents environnements :

  • Différentes plates-formes d’exécution.
  • Différents outils de compilation.
  • Diverses bibliothèques de base.

Exécuter : les risques d’erreurs propres à cette phase deviennent de plus en plus faibles, voire négligeables, étant donné les progrès faits en matière d’informatique. Cependant, c’est durant cette phase que toutes les erreurs non corrigées dans les phases précédentes deviendront visibles :

  • Un besoin client mal implémenté.
  • Un défaut de conformité aboutissant à un plantage.
  • Tout autre comportement anormal ayant des conséquences plus ou moins graves ou visibles.

Ainsi, chacune des étapes évoquées comporte un nombre important d’erreurs possibles. Il devient vital pour le projet de les valider car chacune d’entre elles peut faire dévier considérablement de l’objectif. Le test devient alors un outil de contrôle du processus de fabrication du logiciel. Dans une vision logique, le système pourrait être représenté ainsi :

Boucle de feedback des tests

Boucle de feedback des tests

Le test permet une boucle de rétro-action et donne du feedback sur la qualité de ce qui est produit.

Et vous? Pourquoi testez-vous?