I. Introduction▲
L'objectif principal des tests est de garantir la qualité du code de production en permettant des feed back rapides au moment du refactoring. Il est malheureusement très courant de tomber sur du code de test sale, très sale, et des tests mal faits. L'une des situations où l'on peut rencontrer des problèmes de lisibilité c'est quand on a à tester des exceptions. Junit fournit pour cela différents patterns. Nous allons exposer leurs limites à travers un exemple simple et montrer comment on peut faire beaucoup plus simple avec la librairie Catch-exception.
II. Exemple de code à tester▲
public
class
Calculator {
public
double
squareRoot
(
int
x)throws
IllegalArgumentException{
if
(
x<
0
){
throw
new
IllegalArgumentException
(
"Could not calculate square root of a negative number"
);
}
else
{
return
sqrt
(
x);
}
}
public
double
divide
(
int
x, int
y) throws
IllegalArgumentException{
if
(
y==
0
){
throw
new
IllegalArgumentException
(
"Could not divide by 0"
);
}
else
{
return
x/
y;
}
}
}
Notre calculateur définit deux opérations qui déclarent toutes deux l'exception IllegalArgumentEception, avec des messages différents.
III. fail() avec try catch▲
@Test
public
void
exp0_should_throw_exception_when_calculating_square_root_of_negative_number
(
){
try
{
calculator.squareRoot
(-
10
);
fail
(
"Should throw exception when calculating square root of a negative number"
);
}
catch
(
IllegalArgumentException aExp){
assert
(
aExp.getMessage
(
).contains
(
"negative number"
));
}
}
Le test passe si l'exception spécifiée dans le try-catch est levée, sinon il échoue à l'exécution de la méthode fail() avec le message suivant « Should throw exception when calculating square root of a negative number ». On peut ensuite ajouter des assertions supplémentaires dans le bloc catch. C'est un pattern ad hoc qui n'est pas dédié au test, il peut rendre le code de test moins lisible.
IV. Expected Annotation▲
@Test
(
expected=
IllegalArgumentException.class
)
public
void
exp1_should_throw_exception_when_calculating_square_root_of_negative_number
(
) {
calculator.divide
(
100
,0
);
calculator.squareRoot
(-
10
);
}
Ici le code est beaucoup plus concis. Le test passe quand l'exception spécifiée dans la propriété expected de l'annotation @Test est levée. On rencontre une des limites de cette approche dans le cas où on a plusieurs instructions susceptibles de lever le même type d'exception. Dans l'exemple ci-dessus, on ne sait pas forcément de quelle ligne vient l'exception. Il est aussi impossible de faire des assertions sur les états des objets utilisés dans le test. On ne peut pas non plus faire des tests sur les propriétés de l'exception, comme le message ou le code d'erreur, qui peuvent être utiles pour lever l'ambiguïté dans certaines situations. Ce pattern n'est donc à utiliser que pour des cas très simples.
V. JUnit @Rule et ExpectedException▲
@Rule
public
ExpectedException thrown =
ExpectedException.none
(
);
@Test
public
void
exp2_should_throw_exception_when_calculating_square_root_of_negative_number
(
){
thrown.expect
(
IllegalArgumentException.class
);
thrown.expectMessage
(
JUnitMatchers.containsString
(
"negative number"
));
calculator.squareRoot
(-
10
);
}
On commence par déclarer thrown qui peut être utilisé dans toutes les méthodes de test. Contrairement à Expected on peut faire une assertion sur le contenu du message, avec des matchers de Junit si on veut avoir plus de précisions. Le message suivant s'affichera si aucune exception n'est levée « Expected test to throw (exception with message a string containing"negative number"and an instance of java.lang.IllegalArgumentException ». Mais là non plus, aucune assertion n'est possible sur les états des objets utilisés.
VI. Catch-exception▲
http://code.google.com/p/catch-exception/
@Test
public
void
exp3_should_throw_exception_when_calculating_square_root_of_negative_number
(
){
catchException
(
calculator).squareRoot
(-
10
);
assert
caughtException
(
) instanceof
IllegalArgumentException;
}
Dépendance Maven
<dependency>
<groupId>
com.googlecode.catch-exception</groupId>
<artifactId>
catch-exception</artifactId>
<version>
1.2.0</version>
<scope>
test</scope>
</dependency>
La grande différence qu'elle apporte par rapport aux précédents exemples est qu'elle respecte le très commun pattern Arrange-Act-Assert. Elle est externe à Junit et peut être utilisée avec d'autres frameworks de test comme Test NG. Le code est concis et facile à lire. On peut savoir à quel appel générer l'exception, tester plusieurs exceptions à la fois et ajouter des assertions sur les états des objets et les propriétés des exceptions.
N'hésitez pas à lire la documention pour explorer toute sa richesse et sa simplicité, et son utilisation avec les matchers.
VII. Remerciements▲
Cet article a été publié avec l'aimable autorisation de la société Arolla. L'article original (Catch-Exception : pour tester vos exceptions sur JUnit) peut être vu sur le blog/site de Arolla.
Nous tenons à remercier Fabien pour sa relecture orthographique attentive de cet article et Régis Pouiller pour la mise au gabarit.