Tests de classes en Java

Dans la vie d'un logiciel les tests interviennent à deux périodes; d'une part pendant la longue phase de construction, par petites étapes successives, de chaque classe, et d'autre part pendant l'écriture de nouvelles fonctionalités, qui ont été demandées par les utilisateurs satisfaits de la version initiale.

Pendant la programmation initiale, on peut indiquer schématiquement que chaque méthode ajoutée doit avoir son jeu de tests; ainsi pour une classe d'une dizaine de méthodes on arrive facilement à une trentaine de tests. Lorsque, plus tard, de nouvelles fonctionalités sont ajoutées, il faut évidemment soumettre à nouveau cette trentaine de tests initiaux pour s'assurer que les fonctionalités initiales sont toujours assurées; on utilise les termes de tests de non-régression.

L'outil JUnit automatise l'enchaînement de tests; le programmeur conçoit et programme chaque test, en observant quelques règles simples puis JUnit exécute ces tests et fournit un compte rendu.
Il faut réaliser que JUnit ne crée pas de test. Pour une méthodologie d'élaboration de tests, voir les références en fin de document.

I. Premier exemple

Cet exemple présente une classe particulièrement simple et un test (une autre classe), pour illustrer l'aide apportée par JUnit, et la facilité de son utilisation.
/* Totalisateur */
public class Totalisateur {
private double  tot;
Totalisateur() { tot=0;}
void ajout(double d) { tot += d; } 
double valeur(){ return tot; } 
}

La classe contenant un test des méthodes de 'Totalisateur', appelée TestTotal est:

import junit.framework.*;
import junit.textui.TestRunner;

public class TestTotal extends TestCase {
public void testAjout() {
   Totalisateur t = new Totalisateur();
   assertTrue("constructeur",t.valeur()==0);
   t.ajout(5.5);
   assertEquals("ajout(double d)",5.5,t.valeur(),0.0001);
   }
public static void main (String[] args) {
   TestRunner.run(TestTotal.class);
   }
}

Remarquez les utilisations, du paquetage framework et de la classe TestRunner.

Les commandes suivantes effectuent la conversion en classe; la troisème permet l'exécution du test. Les fichiers java, les classes et l'archive junit.jar sont dans le répertoire courant.
javac                          Totalisateur.java
javac -classpath junit.jar:.   TestTotal.java
java  -cp junit.jar:.          TestTotal

Que fait l'outil JUnit ?

    Dans la classe TestTotal, JUnit intervient plusieurs fois.
  1. Tout d'abord cette classe dérive de TestCase.
  2. Dans la méthode 'testAjout', la méthode assertTrue, de la classe TestCase, affiche le message passé en premier paramètre, si le paramètre suivant est false; ainsi l'utilisateur est averti d'une initialisation incorrecte, dans le constructeur.
    Ensuite la méthode assertEquals utilisée aussi dans 'testAjout', affiche le message passé en paramètre, si deuxième et troisème paramètre ne sont pas égaux. Ainsi l'utilisateur est averti si le résultat fourni par l'appel t.valeur() est différent de la valeur attendue, ici 5.5.
  3. Enfin la méthode run de la classe TestRunner, qui reçoit un nom de classe en paramètre, va exécuter toutes les méthodes publiques de cette classe dont le nom commence par test.

II. Avec deux classes

Voici un cas un peu plus complexe qui montre comment écrire les tests pour que JUnit les enchaîne. Nous avons vu que les vérifications sont traduites par des appels assert...; une documentation sur les méthodes de la classe Assert est accessible en ligne.

Dans cet exemple, nous avons deux classes, Monnaie et PorteMonnaie, suivie de deux classes contenant les tests, respectivement TestMonnaie et TestPorteMonnaie. Chaque classe est dans son propre fichier; il est nécessaire de séparer les classes en développement et les classes de test, car les premières seules seront communiquées aux autres programmeurs.

Les premiers tests, placés dans 'TestMonnaie.java' ne concernent que la classe 'Monnaie'; quand on crée, ensuite, la classe 'PorteMonnaie', on crée également la classe de test associée 'TestPorteMonnaie'; on ajoute alors une ligne dans la fonction 'main' de la classe 'TestMonnaie' pour enchainer ces nouveaux tests aux précédents.

Classe Monnaie et sa classe de tests

La classe Monnaie mémorise une devise et un certain montant dans cette devise; par exemple si nous avons 55 francs suisses, la devise est 'CHB' et le montant 55.

/* Monnaie.java */

class Monnaie {
private double qte;
private String devise;

Monnaie( double q, String d) { qte=q; devise=d.toUpperCase(); }
double montant() { return qte;}

void ajouter(double q) { qte+=q; }

public boolean memeDevise(Monnaie m) { return m.devise==devise; }
// public boolean memeDevise(Monnaie m) {
//    return devise.equals(m.devise); }
//    }

public boolean equals(Object obj) {
   if(obj instanceof Monnaie) {
      Monnaie m = (Monnaie)obj;
      return qte==m.qte && memeDevise(m);
      }
   else return false;
   }
public String toString() { return qte+" "+devise; }
}

Dans la classe TestMonnaie sont définis les tests relatifs à la classe Monnaie; c'est la seule classe contenant une méthode 'main'.

/* TestMonnaie.java */
// export CLASSPATH=.:~/app/java/ju/dist/junit.jar
// javac Monnaie.java TestMonnaie.java

import junit.framework.*;
import junit.textui.TestRunner;

import java.util.Vector;

public class TestMonnaie extends TestCase {
Monnaie chf56;
Monnaie m;

// public TestMonnaie(String ch) {super(ch);}

protected void setUp() { 
  chf56 = new Monnaie(56,"CHF");
  m = new Monnaie(56,"chf");
  }

public void testMemeDevise() {
   assertTrue("\nEgalité",chf56.memeDevise(chf56));
   assertTrue("\nMême devise",chf56.memeDevise(m));
   }

public void testEquals() {
   Monnaie m = new Monnaie(56,"chf");
   assertTrue("\nEgalité prévue",chf56.equals(m));
   }

public void testAjouter() {
   m.ajouter(4);
   assertTrue("\najouter(4)",m.montant()==60);
   }

public static void main(String mots[]) {

   // TestSuite s = new TestSuite(TestPorteMonnaie.class);
   TestSuite s = new TestSuite(TestMonnaie.class);
   s.addTestSuite(TestPorteMonnaie.class);
   TestRunner.run(s);
   }

/* non utilisée ici; (symétrique de setUp()) */
protected void tearDown() { }
}

Classe PorteMonnaie et sa classe de tests

La classe PorteMonnaie permet de grouper plusieurs monnaies; on pourra ajouter une certaine 'monnaie' dans un 'PorteMonnaie'. Si la devise n'y est pas, elle est simplement ajoutée; si elle y est, le montant est augmentée de la quantité versée.
La classe Vector est utilisée pour stocker plusieurs monnaies, de différentes devises.

/* PorteMonnaie.java */

import java.util.Vector;

public class PorteMonnaie {
private Vector lesM;    // les monnaies suivant différentes devises 
PorteMonnaie() { lesM = new Vector(); }

/** Renvoie -1 ou l'index dans lesM de la devise de la monnaie m */
int indexDevise(Monnaie monnaie) {
   int i, idx; Monnaie m;
   for(i=0, idx=-1; i<lesM.size() && idx==-1; i++) {
      m = (Monnaie)lesM.elementAt(i);
      if( monnaie.memeDevise(m) ) idx=i;
      }
   return idx;
   }
public void ajouter(Monnaie m) {
   int idx = indexDevise(m);
   // Une devise de plus ou cumuler le montant
   if( idx == -1 ) lesM.addElement(m);
   else   monnaie(idx).ajouter( m.montant() );
   }
/** Renvoie la monnaie à l'index idx */
protected Monnaie monnaie(int idx)
                  throws ArrayIndexOutOfBoundsException {
   return  (Monnaie)lesM.elementAt(idx);
   }
public String toString() { 
   String ch=""; int i; Monnaie u;
   for(i=0; i<lesM.size(); i++) ch +="  "+(Monnaie)lesM.elementAt(i);
   return ch;
   }
}

La classe TestPorteMonnaie contient les tests effectués sur la classe 'PorteMonnaie'.

/* TestPorteMonnaie.java */
//   export CLASSPATH=.:~/app/java/ju/dist/junit.jar
//   javac Monnaie.java TestMonnaie.java

import junit.framework.*;
import junit.textui.TestRunner;

public class TestPorteMonnaie extends TestCase {
PorteMonnaie pm, pm2;
Monnaie chf56 = new Monnaie(56,"CHF");

// public TestPorteMonnaie() {super();}

protected void setUp() { 
  pm = new PorteMonnaie();
  pm2 = new PorteMonnaie();
  chf56 = new Monnaie(56,"CHF");
  }

public void testIndexDevise() {
  assertTrue("testIndexDevise",-1==pm.indexDevise(chf56));
  pm.ajouter(chf56);
  assertTrue("IndexDevise doit etre 0",0==pm.indexDevise(chf56));
  }

public void testAjouter() {
  Monnaie eur10 = new Monnaie(10,"eur");
  pm2.ajouter(eur10);
  int idx= pm2.indexDevise(eur10);
  Monnaie m = pm2.monnaie(idx);
  assertEquals(" Ajouter: 10",10, m.montant(),0);
  pm2.ajouter(eur10); m = pm2.monnaie(idx);
  assertEquals(" Ajouter: 10 -> 20",20, m.montant(),0);
  }
}

Les commandes

Les commandes pour compiler et utiliser les classes sont ci-après; on remarque que l'emplacement de l'archive contenant les classes de JUnit est précisé pour compiler ou utiliser TestMonnaie.
javac Monnaie.java
javac -classpath ../dist/junit.jar:. TestMonnaie.java
java  -classpath ../dist/junit.jar:. TestMonnaie
Pour utiliser et tester la classe PorteMonnaie, on peut, après avoir défini la variable CLASSPATH, taper les commandes:
export CLASSPATH=../dist/junit.jar:.    # UNIX sh, ksh, bash
javac  PorteMonnaie.java  TestPorteMonnaie.java
java   TestPorteMonnaie

III. Affichage

Pour illustrer les informations fournies par JUnit, on a laissé une erreur dans la classe Monnaie (voir le texte ci-dessus).
Quand on soumet la classe TestMonnaie à JUnit (par la ligne de commande) l'affichage obtenu est le suivant:
There were 2 failures:
1) testEquals(TestMonnaie)junit.framework.AssertionFailedError: 
Egalité prévue
        at TestMonnaie.testEquals(TestMonnaie.java:26)
2) testMemeDevise(TestMonnaie)junit.framework.AssertionFailedError: 
Egalité prévue
        at TestMonnaie.testMemeDevise(TestMonnaie.java:21)

FAILURES!!!
Tests run: 3,  Failures: 2,  Errors: 0
Observons:

IV. En bref

  1. Ecrire des classes, en plaçant des assertions dans des méthodes publiques dont le nom commence par test.
        assertTrue("constructeur",t.valeur()==0);
        assertEquals("ajout(double)",5.5,t.valeur(),0.0001);
        assertEquals(" Ajouter -> 10",10, m.montant);
  2. Réunir dans une suite de tests (classe TestSuite) ces noms de classe de test.
        TestSuite s = new TestSuite(TestMonnaie.class);
        s.addTestSuite(TestPorteMonnaie.class);
  3. Lancer cette suite de tests.
        TestRunner.run(s);

Nous n'avons pas mentionné ici le lancement de tests dans une fenêtre dédiée, effectué en utilisant l'une des classes junit.awtui.TestRunner ou junit.swingui.TestRunner.
Avec un tel lancement, le point 3 ci-dessus est sans objet.

Références