Tutoriel sur Catch-Exception (pour tester vos exceptions sur JUnit)

| Yakhya Dabo (Arolla) | Arolla |

Cet article se propose de présenter la librairie Catch-Exception qui permet de tester les exceptions sur JUnit.

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum Commentez .

- Titre : Tutoriel sur Catch-Exception (pour tester vos exceptions sur JUnit)

- Auteurs : Yakhya Dabo (Arolla), Arolla

- Parution : 8 septembre 2014

- Licence : Copyright ® 2014 Arolla. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Exemple de code à tester

- III. fail() avec try catch

- IV. Expected Annotation

- V. JUnit @Rule et ExpectedException

- VI. Catch-exception

- VII. Remerciements

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.