4 Jartege | Table des matières |
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.
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
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)
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.
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
javac Gen.java
java Gen
File TestSequence.java generated.
javac TestSequence.java
java TestSequence
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: 0Ici, 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.
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.
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)
className
avec la fonction
spécifiée.
CreationProbability
est une interface qui contient
une unique méthode
double theFunction(int nbCreatedObjects)
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
.
À 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
.
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() ;
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.
4 Jartege | Table des matières |