5 RéférencesPage principale du tutoriel3.3 Un programme très simple de test4 JartegeTable des matières

4 Jartege

4.1 Test de conformité avec JML

Le test de conformité d'un programme consiste à exécuter ce programme en vérifiant que l'exécution est conforme à sa spécification. On parle également de « test basé sur les spécifications », ou « specification-based testing ». Le test de conformité d'un programme peut être réalisé en reformulant les spécifications du programme sous forme d'assertions incluses et évaluées lors de son l'exécution. Lorsqu'une assertion est évaluée à faux, cela signifie qu'une spécification n'est pas respectée, et qu'il y a donc une erreur dans le programme ou dans la spécification.

Si le programme est spécifié en JML, on peut vérifier que ce programme est conforme à sa spécification en évaluant les assertions JML. Cela peut être réalisé très simplement en utilisant le compilateur d'assertions JML (jmlc). Si une des assertions est évaluée à faux, une exception spécifique est levée et on en déduit qu'il y a une erreur dans le programme ou dans la spécification.

Les assertions JML sont donc utilisées comme oracle de test. Cet oracle peut être plus ou moins bon selon le degré de spécification du programme. Si le programme est peu spécifié, c'est-à-dire comporte des assertions JML qui posent peu de contraintes, comme des post-conditions réduites à true, l'oracle ne sera pas performant et ne permettra pas de mettre en évidence certaines erreurs. Par contre, si les assertions JML posent des contraintes suffisantes, cet oracle pourra mettre en évidence un maximum d'erreurs.

L'utilisation de JML comme oracle de test a été en particulier implantée dans l'outil jmlunit [CL02], qui permet de générer des tests unitaires pour des méthodes Java. Ces test unitaires, conçus pour pouvoir être exécutés avec JUnit, réalisent une série de tests constitués essentiellement d'un appel à une méthode.

4.2 L'outil Jartege

Pour effectuer des tests unitaires de classes Java, il est nécessaire d'écrire des programmes de test qui appellent les opérations des classes que l'on souhaite tester, appelées « classes sous test ». L'écriture de programmes de test est une activité qui prend beaucoup de temps, et qui n'est pas très intéressante.

La génération automatique de programmes de test a pour but de produire facilement un grand nombre de tests, afin de réduire l'effort nécessaire au développement d'une campagne de test.

L'outil Jartege (Java Random Test Generator) permet de générer automatiquement des tests aléatoires pour des classes Java spécifiées en JML. Cette approche consiste à produire des programmes de test composés de suites aléatoires d'appels d'opérations des classes sous test. Chaque programme de test peut ensuite être exécuté pour tester le programme, puis ré-exécuté ultérieurement soit après la correction d'une erreurs, soit pour effectuer des tests de non régression.

L'outil est conçu pour produire des tests unitaires, c'est-à-dire des tests qui comportent des appels à certaines méthodes appartenant à un petit nombre de classes. Dans la mesure où l'on peut d'une part effectuer un grand nombre d'appels de méthodes, et d'autre part tester ensemble plusieurs classes, le type de test effectué avec Jartege se situe à la limite du test d'intégration.

Le test aléatoire est considéré ici comme un moyen complémentaire de production de programmes de test ; il ne vise pas à remplacer la production manuelle de jeux de tests, mais à mettre en évidence un certain nombre d'erreurs avec un effort minimal. Le test aléatoire peut en particulier

4.3 Un exemple d'utilisation de Jartege

Supposons que l'on souhaite tester la classe Sequence. On écrit pour cela le programme Java suivant :


/**
 * Classe permettant de générer des tests pour la classe Sequence
 * avec l'outil Jartege.
 */

import jartege.* ;

class Gen {

   public static void main(String[] args) {
      // Crée un testeur de classes
      ClassTester t = new ClassTester() ;

      // Ajoute la classe Sequence à l'ensemble des classes sous test.
      t.addClass("Sequence") ;

      // Génère une classe de test TestSequence, qui comporte 10 méthodes
      // de test ; chaque méthode de test effectuant 40 tentatives d'appels.
      t.generate("TestSequence", 10, 40) ;
   }
}
   

La classe principale de Jartege est ClassTester. Cette classe doit être instanciée pour permettre la génération de programmes de test.

La méthode addClass(String className) permet d'ajouter la classe className à l'ensemble des classes sous test.

La méthode

generate(String className, int numberOfTests, int numberOfMethodCalls)

génère un fichier Java qui comporte une classe de nom className, numberOfTests méthodes de test. Pour chaque méthode de test, l'outil effectue numberOfMethodCalls tentatives de génération d'un appel d'opération.

L'exécution du programme précédent produit un fichier TestSequence.java. Ce fichier comporte 10 méthodes de test : test1, test2, ... test10. Chaque méthode de test comporte environ 40 appels de méthodes.

Lors de la génération du programme de test, chaque appel de méthode est exécuté à la volée, ce qui permet de d'éliminer les appels qui ne respectent pas la pré-condition de la méthode. Il peut arriver que l'outil ne parvienne pas à générer d'appel pour une méthode, ce qui explique que « environ » 40 appels soient produits.

Exécution du programme de génération

Avant d'exécuter le programme de génération, il faut compiler les fichiers Liste.java et Sequence.java avec les assertions JML associées.

jmlc Liste.java Sequence.java

On compile et on exécute le programme de génération avec le compilateur Java standard :

javac Gen.java
java Gen

Le programme affiche :

File TestSequence.java generated.

Il faut ensuite compiler et exécuter le programme de test généré :

javac TestSequence.java
java TestSequence

Le programme affiche alors un message pour chaque erreur rencontrée, puis affiche un bilan. Ici on obtient le résultat suivant (qui sera évidemment différent pour chaque programme de test généré).

1) Error detected in class TestSequence by method test1: 
        org.jmlspecs.jmlrac.runtime.JMLPostconditionError: 
By method "contient" of class "Sequence" for assertions specified at 
        Liste.java:71:22, when
                'val' is 0
                '\result' is false
                'this' is Sequence@f33675
        at Sequence.checkPost$contient$Sequence(Sequence.java:1068)
        at Sequence.contient(Sequence.java:1149)
        at TestSequence.test1(TestSequence.java:75)
        at TestSequence.main(TestSequence.java:23)

2) Error detected in class TestSequence by method test2:

[...]

3) Error detected in class TestSequence by method test8:

[...]

4) Error detected in class TestSequence by method test9:

[...]

Test file: TestSequence
Number of tests: 10
Number of errors: 4
Number of inconclusive tests: 0   
Ici, le programme a détecté 4 erreurs. En réalité, l'examen de la trace de la pile montre qu'il s'agit probablement à chaque fois de la même erreur. Cette erreur est mise en évidence par la post-condition de la méthode contient.

Le programme de test indique également le nombre de tests « inconclusifs ». Un test est inconclusif lorsqu'il ne permet pas de conclure si le programme a un comportement correct ou non. Jartege indique qu'un test est inconclusif lorsqu'il contient un appel d'opération qui ne respecte pas une pré-condition. Dans la mesure où Jartege évite de générer de tels appels, il peut arriver qu'un appel d'opération ne respecte pas une pré-condition uniquement lorsqu'une des classes sous test (code Java ou spécification JML) a été modifiée entre le moment où le test a été généré et le moment où ce test est exécuté. Un nombre important de tests inconclusifs indique que le fichier de test n'est plus pertinent.

4.4 Contrôle de la génération aléatoire

Si on génère des programmes de test de façon complètement aléatoire, on risque de ne pas générer des séquences d'appels très intéressantes. Jartege offre donc plusieurs possibilité pour paramétrer l'aspect aléatoire de l'outil.

Création d'objets

Selon la classe sous test considérée, il peut être intéressant de créer soit beaucoup d'instances de cette classe, soit au contraire d'en créer peu et d'effectuer beaucoup d'appels de méthodes sur ce petit nombre d'instances. Dans l'exemple des séquences, il n'est pas très intéressant de créer beaucoup d'instances de la classe Sequence. On peut tester le programme plus efficacement en créant une seule instance et en effectuant de nombreux appels de méthodes sur cette instance.

Pour contrôler la création d'objets, Jartege permet d'associer à toute classe une fonction de probabilité de création. Une fonction de probabilité de création définit la probabilité de créer un nouvel objet de la classe en fonction du nombre d'objets existants de cette classe. Si cette probabilité est faible, Jartege a plus de chances de réutiliser un objet déjà créé.

La fonction

changeCreationProbability(String className, CreationProbability function)

change la fonction de probabilité de création de la classe className avec la fonction spécifiée. CreationProbability est une interface qui contient une unique méthode

double theFunction(int nbCreatedObjects)

telle que theFunction(0) = 1 et theFunction(n)  [0, 1].

La classe ThresholdProbability permet de définir des fonctions de probabilité de création qui valent 1 en dessous d'un certain seuil s, et 0 au dessus. Cela permet de spécifier qu'une méthode de test crée au plus s instances d'une classe.

On peut affiner le programme de génération de tests pour la classe Sequence de la façon suivante :


/**
 * Classe permettant de générer des tests pour la classe Sequence
 * avec l'outil Jartege.
 */

import jartege.* ;

class Gen {

   public static void main(String[] args) {
      
      ClassTester t = new ClassTester() ;
      t.addClass("Sequence") ;

      // Associe à la classe Sequence la probabilité seuil égale à 1, ce qui signifie qu'une
      // méthode de test générée construira au plus une instance de la classe Sequence.
      t.changeCreationProbability("Sequence", new ThresholdProbability(1)) ;
 
      t.generate("TestSequence", 10, 40) ;
   }
}
   

Avec ce programme de test, on spécifie que chaque méthode de test construira au plus une instance de la classe Sequence.

Lorsqu'on exécute ce programme de génération, on obtient un nouveau fichier de test TestSequence.java. L'exécution de ce fichier de test produit le résultat suivant :


1) Error detected in class TestSequence by method test3: 
        java.lang.ArrayIndexOutOfBoundsException
        at Sequence.internal$add(Sequence.java:110)
        at Sequence.add(Sequence.java:760)
        at TestSequence.test3(TestSequence.java:209)
        at TestSequence.main(TestSequence.java:25)

2) Error detected in class TestSequence by method test4: 
        org.jmlspecs.jmlrac.runtime.JMLPostconditionError: 
By method "contient" of class "Sequence" for assertions specified at 
        Liste.java:71:22, when
                'val' is 1711694703
                '\result' is false
                'this' is Sequence@10b053
        at Sequence.checkPost$contient$Sequence(Sequence.java:1068)
        at Sequence.contient(Sequence.java:1149)
        at TestSequence.test4(TestSequence.java:229)
        at TestSequence.main(TestSequence.java:26)

3) Error detected in class TestSequence by method test6:

[...]

4) Error detected in class TestSequence by method test7:

[...]

5) Error detected in class TestSequence by method test9:

[...]

6) Error detected in class TestSequence by method test10:

Test file: TestSequence
Number of tests: 10
Number of errors: 6
Number of inconclusive tests: 0   

On remarque donc qu'une nouvelle erreur est détectée. Cette erreur n'est pas causée par la violation d'une assertion JML, mais par la levée de l'exception ArrayIndexOutOfBoundsException.

Poids associés aux classes et aux opérations

À toute classe et à toute opération est associé un poids, qui définit la probability de choix d'une classe et d'une opération dans la classe choisie. En particulier, il est possible d'interdire l'appel d'une opération en lui associant un poids nul.

Par défaut, tous les poids sont égaux à 1. Un poids peut être modifié à l'aide d'une méthode de modification des poids.

La méthode changeClassWeight(String className, double weight) permet de changer le poids de la classe spécifiée.

La méthode changeMethodWeight(String className, String methodName, double
weight) permet de changer le poids de la méthode methodName dans la classe className.

La méthode changeAllMethodsWeight(String className, double weight) permet de changer le poids de toutes les méthodes de la classe className.

On peut par exemple tester uniquement la méthode add de la classe Sequence à l'aide du programme de génération de tests suivant :


/**
 * Classe permettant de générer des tests pour la classe Sequence
 * avec l'outil Jartege.
 */

import jartege.* ;

class Gen {

   public static void main(String[] args) {
      ClassTester t = new ClassTester() ;
      t.addClass("Sequence") ;
      t.changeCreationProbability("Sequence", new ThresholdProbability(1)) ;
      // Modification des poids des méthodes de la classe Sequence
      t.changeAllMethodsWeight("Sequence", 0) ;
      t.changeMethodWeight("Sequence", "add", 1) ;
      t.generate("TestSequence", 10, 40) ;
   }
}
   

Avec ce programme, une méthode de test contiendra uniquement un appel au constructeur de Sequence et une série d'appels à la méthode add.

Génération de paramètres de types primitifs

Lorsqu'une méthode a une pré-condition très forte, il est peu probable que l'outil parvienne, sans indication supplémentaire, à générer un appel à cette méthode qui respecte la pré-condition.

Cette constatation s'applique particulièrement aux types primitifs. Par exemple, la méthode element(int i) a pour pré-condition

1 <= i && i <= taille() ;

Si Jartege tire une valeur aléatoire dans tout le domaine de int, la probabilité de tirer une valeur dans l'intervalle [1, taille()] est très faible.

Jartege offre la possibilité de définir des méthodes qui génèrent des valeurs de paramètres pour les types primitifs (types boolean, byte, char, double, float, int, long, short) et le type String.

Les fonctions de génération de paramètres pour des opérations d'une classe ClasseA doivent appartenir à une classe JRT_ClasseA. Dans l'exemple des séquences, on peut définir la classe JRT_Sequence suivante :


/**
 * Classe qui contient des méthodes de génération des paramètres
 * pour des opérations de la classe Sequence.
 */

import jartege.RandomValue ;

public class JRT_Sequence {

   // La séquence courante, sur laquelle l'opération est appliquée
   private Sequence laSequence ;

   /**
    * Constructeur.
    */
   public JRT_Sequence(Sequence laSequence) {
      this.laSequence = laSequence ;
   }

   /**
    * Méthode qui génère le premier paramètre de element(int i).
    */
   public int JRT_element_int_1() {
      return RandomValue.intValue(1, laSequence.taille()) ;
   }
}
   

Cette classe contient un attribut de type Sequence, appelé ici laSequence. Cet attribut représente la séquence courante sur laquelle est appliquée une opération de la classe Sequence.

La classe JRT_Sequence contient un constructeur qui a un paramètre de type Sequence permettant d'initialiser l'attribut.

La méthode JRT_element_int_1 permet de générer le premier (et unique) paramètre de la méthode element(int i). Le type des paramètres est utilisé dans le nom de cette méthode (JRT_element_int_1) afin de pouvoir traiter la surcharge de méthodes. La méthode intValue(int min, int max), définie dans la classe jartege.RandomValue, effectue un tirage aléatoire d'un entier compris entre min et max. Ici, on tire donc un entier compris entre 1 et la taille de la séquence sur laquelle est appliquée la méthode element, ce qui garantit d'obtenir une valeur qui satisfait la pré-condition.


5 RéférencesPage principale du tutoriel3.3 Un programme très simple de test4 JartegeTable des matières