Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 38

Maîtriser assertThrows pour la Gestion des Exceptions

Recommended resources

Bonjour ! Dans la leçon précédente, nous avons appris à structurer nos tests de manière propre et isolée grâce aux annotations de cycle de vie comme @BeforeEach et @AfterEach. Vous savez maintenant comment garantir que chaque test s'exécute dans un environnement contrôlé.

Aujourd'hui, nous allons nous concentrer sur un aspect tout aussi crucial de la robustesse d'une application : la gestion des erreurs. Un bon code ne se contente pas de fonctionner avec des données valides (le "happy path"), il doit aussi réagir correctement face à des données invalides. L'objectif de cette leçon est de vous apprendre à tester que votre code lève bien les exceptions attendues dans ces cas d'erreur, en utilisant la méthode assertThrows de JUnit.

1. Pourquoi tester les exceptions ?

En tant que développeur, vous savez que les utilisateurs ou d'autres systèmes peuvent fournir des données inattendues. Une application robuste doit anticiper ces scénarios. Plutôt que de laisser l'application planter, une bonne pratique est de lancer une exception pour signaler un problème.

Par exemple :

  • Tenter de diviser un nombre par zéro.
  • Chercher un utilisateur en base de données avec un ID qui n'existe pas.
  • Créer un objet avec un paramètre invalide (par exemple, un nom de produit vide).

Vos tests unitaires doivent couvrir ces "unhappy paths" (scénarios d'erreur) pour garantir que votre code est non seulement correct, mais aussi résilient. Ne pas tester les cas d'erreur, c'est laisser une porte ouverte à des bugs en production.


2. La méthode assertThrows

Pour vérifier qu'une portion de code lève bien une exception, JUnit 5 fournit une méthode d'assertion très élégante : assertThrows.

Cette image résume les assertions les plus courantes de JUnit, incluant `assertThrows` qui est notre focus aujourd'hui.

La syntaxe de assertThrows est la suivante :

assertThrows(Class<T> expectedException, Executable executable)

Décortiquons ses deux arguments :

  1. expectedException : La classe de l'exception que l'on s'attend à voir levée. Par exemple, IllegalArgumentException.class.
  2. executable : Le bloc de code qui est censé provoquer l'exception. Pour cela, on utilise une expression lambda.

Le rôle crucial de l'expression lambda

Vous vous demandez peut-être pourquoi on utilise la syntaxe () -> monCodeQuiPlante().

Si vous écriviez assertThrows(IllegalArgumentException.class, service.maMethode(valeurInvalide)), la méthode maMethode serait exécutée avant l'appel à assertThrows. L'exception serait donc levée immédiatement, faisant échouer le test sans que assertThrows n'ait eu la chance de la "capturer".

La lambda () -> ... encapsule l'appel de votre méthode. Elle ne l'exécute pas tout de suite. Elle passe à assertThrows une "recette" pour exécuter le code. En interne, assertThrows peut alors l'envelopper dans un bloc try-catch pour vérifier si la bonne exception est bien attrapée.


3. Mise en pratique : Tester une division par zéro

L'exemple le plus classique est la division par zéro. Voyons comment le tester concrètement. Le tutoriel suivant vous guide pas à pas.

Utilisation de assertThrows dans JUnit 5 pour les Tests d ...

Cet article de Byoskill montre un exemple très clair et concis de test d'exception.

Lisez les sections suivantes : "Création d'une classe avec une méthode susceptible de lever une exception": Repérez comment la classe Calculatrice est implémentée, en particulier la méthode diviser qui lance une IllegalArgumentException si le diviseur est zéro. "Création d'un test utilisant assertThrows": Analysez la classe de test CalculatriceTest. Concentrez-vous sur la méthode testDivisionParZero et la syntaxe de assertThrows pour vérifier que l'exception est bien levée.

Comme vous l'avez lu, le test est simple et direct :

@Test
public void testDivisionParZero() {
    Calculatrice calculatrice = new Calculatrice();
    
    // On affirme (assert) que l'exécution de calculatrice.diviser(10, 0)
    // va lever (throws) une exception de type IllegalArgumentException.
    assertThrows(IllegalArgumentException.class, () -> {
        calculatrice.diviser(10, 0);
    });
}

Si l'exception est levée, le test passe (vert). Si aucune exception n'est levée, ou si une autre sorte d'exception est levée, le test échoue (rouge).


4. Démonstration en vidéo

Pour voir ce concept en action, la vidéo ci-dessous montre un autre exemple pratique. Le présentateur teste une classe Character et vérifie qu'une exception est levée si le nom du personnage est invalide.

Tutoriel Java - tests unitaires

Cette vidéo, que vous avez déjà consultée pour d'autres concepts, montre ici l'utilisation de assertThrows.

Regardez la portion de test d'exception. Observez comment le présentateur : Identifie un cas d'erreur à tester (un nom de personnage trop court). Utilise assertThrows avec le type d'exception attendu (IllegalArgumentException.class). Passe le code qui doit échouer via une expression lambda. Exécute les tests et analyse le résultat.


5. Capturer et inspecter l'exception (Avancé mais utile)

Parfois, il ne suffit pas de savoir qu'une exception a été levée. On veut aussi vérifier son message. assertThrows retourne l'instance de l'exception capturée, ce qui vous permet de faire des assertions supplémentaires sur elle.

Dans cet exemple plus complexe, `assertThrows` est utilisé pour s'assurer qu'une `UsernameNotFoundException` est levée. On pourrait ensuite inspecter l'exception retournée.

Voici comment vous pourriez faire pour l'exemple de la calculatrice :

@Test
public void testDivisionParZeroAvecMessage() {
    Calculatrice calculatrice = new Calculatrice();

    // 1. On capture l'exception retournée par assertThrows
    IllegalArgumentException exception = assertThrows(
        IllegalArgumentException.class, 
        () -> {
            calculatrice.diviser(10, 0);
        }
    );

    // 2. On fait une assertion sur le message de l'exception
    String expectedMessage = "Division par zéro";
    String actualMessage = exception.getMessage();
    assertEquals(expectedMessage, actualMessage);
}

C'est une technique très puissante pour garantir que vos messages d'erreur sont clairs et corrects, ce qui est essentiel pour le débogage en production.


6. Exercice : Rendez votre ProductService plus robuste

Il est temps de mettre la main à la pâte. Dans les leçons précédentes, notre ProductService.findById() retournait null si un produit n'était pas trouvé. Bien que ce soit une approche valide, une autre approche courante dans les API REST est de lever une exception.

Votre mission :

  1. Créez une exception personnalisée :

    • Dans votre projet, créez une nouvelle classe nommée ProductNotFoundException.
    • Faites-la hériter de RuntimeException (nous verrons la différence entre checked et unchecked exceptions plus tard, RuntimeException est un bon choix pour commencer).
    • Ajoutez un constructeur qui prend un message en paramètre et l'envoie au constructeur de la classe mère super(message);.
    public class ProductNotFoundException extends RuntimeException {
        public ProductNotFoundException(String message) {
            super(message);
        }
    }
    
  2. Modifiez le ProductService :

    • Ouvrez la classe ProductService.
    • Dans la méthode findById(Long id), modifiez la logique : au lieu de retourner null si le produit n'est pas trouvé, levez votre nouvelle exception.
    // Dans ProductService.java
    public Product findById(Long id) {
        // ... votre logique de recherche actuelle ...
        if (product == null) {
            // Au lieu de "return null;"
            throw new ProductNotFoundException("Produit non trouvé avec l'ID : " + id);
        }
        return product;
    }
    
  3. Écrivez le test d'exception :

    • Ouvrez votre classe de test ProductServiceTest.
    • Créez une nouvelle méthode de test : void testFindById_ShouldThrowException_WhenProductNotFound().
    • À l'intérieur, utilisez assertThrows pour vérifier que l'appel à service.findById(99L) (ou tout autre ID non-existant) lève bien une ProductNotFoundException.
  4. Nettoyez vos anciens tests :

    • Votre ancien test testFindNonExistingProduct qui vérifiait assertNull va maintenant échouer. C'est normal, le comportement du code a changé !
    • Supprimez ou commentez ce test, car il ne reflète plus la logique actuelle de votre application.

Lancez tous les tests. Ils devraient tous être au vert !


Conclusion

Félicitations ! Vous avez complété le module sur les tests unitaires avec JUnit. Vous savez désormais non seulement tester que votre code fonctionne, mais aussi qu'il échoue de manière prévisible et contrôlée.

Les points clés à retenir :

  • Tester les cas d'erreur (les "unhappy paths") est fondamental pour construire des applications robustes.
  • assertThrows est la méthode standard de JUnit 5 pour vérifier qu'un bloc de code lève une exception spécifique.
  • L'utilisation d'une expression lambda est indispensable pour que assertThrows puisse "capturer" et inspecter l'exception.
  • On peut capturer l'objet exception retourné par assertThrows pour effectuer des vérifications supplémentaires, comme valider son message.

Dans la prochaine leçon, nous commencerons un nouveau module passionnant : la Programmation Fonctionnelle avec les Lambdas et les Streams. Vous avez déjà eu un aperçu de la puissance des lambdas avec assertThrows ; préparez-vous maintenant à les utiliser pour transformer la manière dont vous manipulez les collections de données en Java.