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 pattern. Nous allons exposer leurs limites à travers un exemple simple et montrer comment on peut faire beaucoup plus simple avec la librairie Catch-exception.
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 l'exception IllegalArgumentEception, avec des messages différents. L'idée est d'écrire un test qui détermine avec précision laquelle des exceptions est levée.
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 had-oc qui n'est pas dédié au test, il peut rendre le code de test moins lisible.
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.
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 la classe de tests. 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.
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
com.googlecode.catch-exception catch-exception 1.2.0 test
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 à partir de quel appel l'exception a été générée, 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.
Librairie qui semblait prometteuse mais qui n’est visiblement plus mise à jour puisque la dernière version en date (en octobre 2017) est garantie compatible avec Mockito 1.8 -> 1.9.5 et plante lamentablement avec mon Mockito 2.9.