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.
/* 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
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.
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() { } }
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); } }
javac Monnaie.java javac -classpath ../dist/junit.jar:. TestMonnaie.java java -classpath ../dist/junit.jar:. TestMonnaiePour 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
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: 0Observons:
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.