441 votes

Comment tester qu'aucune exception n'est levée ?

Je sais qu'une façon de le faire serait :

@Test
public void foo() {
   try {
      // execute code that you expect not to throw Exceptions.
   } catch(Exception e) {
      fail("Should not have thrown any exception");
   }
}

Existe-t-il un moyen plus propre de procéder ? (Probablement en utilisant la fonction @Rule ?)

14 votes

Un test JUnit est considéré comme ayant échoué s'il lève une exception autre qu'une exception attendue. En général, aucune exception n'est attendue.

2 votes

N'y a-t-il pas une distinction entre échec et erreur dans JUnit ? Le premier signifie que le test a échoué, le second que quelque chose d'inattendu s'est produit.

2 votes

297voto

olibre Points 6069

JUnit 5 (Jupiter) offre trois fonctions pour vérifier l'absence/la présence d'exceptions :

assertAll​()

Affirme que tous fourni executables
  ne lèvent pas d'exceptions.

assertDoesNotThrow​()

Affirme que l'exécution de la
  fourni executable / supplier
ne jette pas tout type de exception .

  Cette fonction est disponible
  depuis JUnit 5.2.0 (29 avril 2018).

assertThrows​()

Affirme que l'exécution du programme fourni executable
jette une exception de la expectedType
  et renvoie le exception .

Exemple

package test.mycompany.myapp.mymodule;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;

class MyClassTest {

    @Test
    void when_string_has_been_constructed_then_myFunction_does_not_throw() {
        String myString = "this string has been constructed";
        assertAll(() -> MyClass.myFunction(myString));
    }

    @Test
    void when_string_has_been_constructed_then_myFunction_does_not_throw__junit_v520() {
        String myString = "this string has been constructed";
        assertDoesNotThrow(() -> MyClass.myFunction(myString));
    }

    @Test
    void when_string_is_null_then_myFunction_throws_IllegalArgumentException() {
        String myString = null;
        assertThrows(
            IllegalArgumentException.class,
            () -> MyClass.myFunction(myString));
    }

}

280voto

Jeroen Vannevel Points 18676

Tu abordes ça de la mauvaise façon. Testez simplement votre fonctionnalité : si une exception est levée, le test échouera automatiquement. Si aucune exception n'est levée, vos tests seront tous verts.

J'ai remarqué que cette question suscite de temps en temps de l'intérêt, je vais donc m'étendre un peu.

Contexte des tests unitaires

Lorsque vous effectuez des tests unitaires, il est important de définir pour vous-même ce que vous considérez comme une unité de travail. En gros : une extraction de votre base de code qui peut inclure ou non plusieurs méthodes ou classes et qui représente un seul élément de fonctionnalité.

Ou, comme défini dans L'art des tests unitaires, 2e édition par Roy Osherove , page 11 :

A test unitaire est un morceau de code automatisé qui invoque l'unité de travail testée, puis vérifie certaines hypothèses sur un seul résultat final de cette unité. Un test unitaire est presque toujours écrit en utilisant un cadre de test unitaire. Il peut être écrit facilement et s'exécute rapidement. Il est fiable, lisible et facile à maintenir. Il est cohérent dans ses résultats tant que le code de production n'a pas changé.

Ce qu'il faut comprendre, c'est qu'une unité de travail En général, il n'y a pas qu'une seule méthode, mais au niveau le plus élémentaire, il s'agit d'une méthode qui est ensuite encapsulée dans d'autres unités de travail.

enter image description here

L'idéal serait de disposer d'une méthode de test pour chaque unité de travail distincte afin de pouvoir toujours voir immédiatement où les choses ne vont pas. Dans cet exemple, il y a une méthode de base appelée getUserById() qui retournera un utilisateur et il y a un total de 3 unités de travaux.

La première unité de travail doit tester si un utilisateur valide est renvoyé ou non dans le cas d'une entrée valide et invalide.
Toutes les exceptions lancées par la source de données doivent être traitées ici : si aucun utilisateur n'est présent, il doit y avoir un test qui démontre qu'une exception est lancée lorsque l'utilisateur est introuvable. Un exemple de ceci pourrait être le test IllegalArgumentException qui est attrapé avec le @Test(expected = IllegalArgumentException.class) annotation.

Une fois que vous avez traité tous vos cas d'utilisation pour cette unité de travail de base, vous passez au niveau supérieur. Ici, vous faites exactement la même chose, mais vous ne traitez que les exceptions qui proviennent du niveau juste en dessous du niveau actuel. Votre code de test reste ainsi bien structuré et vous permet de parcourir rapidement l'architecture pour trouver où les choses ne vont pas, au lieu de devoir sauter partout.

Traitement des entrées valides et erronées d'un test

À ce stade, il devrait être clair comment nous allons gérer ces exceptions. Il y a 2 types d'entrée : valide l'entrée et défectueux (l'entrée est valide au sens strict, mais elle n'est pas correcte).

Lorsque vous travaillez avec valide tu t'attends implicitement à ce que le test que tu écris fonctionne.

Un tel appel de méthode peut ressembler à ceci : existingUserById_ShouldReturn_UserObject . Si cette méthode échoue (par exemple, si une exception est levée), vous savez que quelque chose ne va pas et vous pouvez commencer à creuser.

En ajoutant un autre test ( nonExistingUserById_ShouldThrow_IllegalArgumentException ) qui utilise le défectueux et attend une exception, vous pouvez voir si votre méthode fait ce qu'elle est censée faire avec une entrée erronée.

TL;DR

Vous essayiez de faire deux choses dans votre test : vérifier les entrées valides et erronées. En divisant cette méthode en deux qui font chacune une chose, vous aurez des tests beaucoup plus clairs et une meilleure vue d'ensemble de ce qui ne va pas.

En gardant à l'esprit l'unité de travail en couches, vous pouvez également réduire la quantité de tests dont vous avez besoin pour une couche supérieure dans la hiérarchie, car vous n'avez pas à tenir compte de tout ce qui aurait pu mal tourner dans les couches inférieures : les couches inférieures à la couche actuelle sont une garantie virtuelle que vos dépendances fonctionnent et si quelque chose ne va pas, c'est dans votre couche actuelle (en supposant que les couches inférieures ne lancent pas d'erreurs elles-mêmes).

7 votes

Le problème est que j'essaie de faire du TDD et que l'un des collaborateurs que j'utilise lève une exception. J'ai donc besoin de tester le fait que je consomme l'exception lancée par le collaborateur.

8 votes

Vous voulez dire que votre fonctionnalité dépend de la gestion d'une exception ? C'est une odeur de code : les exceptions sont là pour vous permettre d'attraper élégamment les problèmes ; elles ne sont pas utilisées pour le contrôle du flux. Si vous voulez tester un scénario dans lequel une exception doit être levée, vous devez utiliser la fonction expected annotation. Si vous voulez tester un scénario où votre code échoue et que vous voulez voir si l'erreur est correctement gérée : utilisez expected et peut-être utiliser des alertes pour déterminer si elle a été résolue.

0 votes

Le problème est que je ne peux pas récupérer l'exception qui s'est produite dans le collaborateur et que je ne fais que consigner le problème à l'aide d'un log.debug("Message d'erreur"). Il n'y a donc pas d'effets secondaires qui se produisent dans le cadre du bloc catch et sur lesquels je peux éventuellement m'appuyer.

260voto

Sven Döring Points 589

Je suis tombé sur ça à cause de la règle "squid:S2699" de SonarQube : "Ajouter au moins une assertion à ce scénario de test".

J'avais un test simple dont le seul but était de passer sans lancer d'exceptions.

Considérez ce code simple :

public class Printer {

    public static void printLine(final String line) {
        System.out.println(line);
    }
}

Quel type d'assertion peut-on ajouter pour tester cette méthode ? Bien sûr, vous pouvez faire un try-catch autour de cette méthode, mais cela ne fait qu'alourdir le code.

La solution vient de JUnit lui-même.

Si aucune exception n'est levée et que vous voulez illustrer explicitement ce comportement, ajoutez simplement expected comme dans l'exemple suivant :

@Test(expected = Test.None.class /* no exception expected */)
public void test_printLine() {
    Printer.printLine("line");
}

Test.None.class est la valeur par défaut pour la valeur attendue.

Si vous import org.junit.Test.None vous pouvez alors écrire :

@Test(expected = None.class)

que vous trouverez peut-être plus lisible.

127voto

denu Points 432

Pour les versions de JUnit antérieures à 5 :

Avec AssertJ assertions fluentes 3.7.0 :

Assertions.assertThatCode(() -> toTest.method())
    .doesNotThrowAnyException();

Mise à jour :

Introduction de JUnit 5 assertDoesNotThrow() Je préfère donc l'utiliser plutôt que d'ajouter une dépendance supplémentaire à votre projet. Voir cette réponse pour les détails.

33voto

Groostav Points 185

Java 8 rend cela beaucoup plus facile, et Kotlin/Scala doublement.

Nous pouvons écrire une petite classe utilitaire

class MyAssertions{
  public static void assertDoesNotThrow(FailingRunnable action){
    try{
      action.run()
    }
    catch(Exception ex){
      throw new Error("expected action not to throw, but it did!", ex)
    }
  }
}

@FunctionalInterface interface FailingRunnable { void run() throws Exception }

et votre code devient alors simple :

@Test
public void foo(){
  MyAssertions.assertDoesNotThrow(() -> {
    //execute code that you expect not to throw Exceptions.
  }
}

Si vous n'avez pas accès à Java-8, j'utiliserais une vieille méthode java : des blocs de code arbitraires et un simple commentaire.

//setup
Component component = new Component();

//act
configure(component);

//assert 
/*assert does not throw*/{
  component.doSomething();
}

Et enfin, avec kotlin, un langage dont je suis récemment tombé amoureux :

fun (() -> Any?).shouldNotThrow() 
    = try { invoke() } catch (ex : Exception){ throw Error("expected not to throw!", ex) }

@Test fun `when foo happens should not throw`(){

  //...

  { /*code that shouldn't throw*/ }.shouldNotThrow()
}

Bien qu'il y ait beaucoup de place pour modifier la façon dont vous voulez exprimer ceci, j'ai toujours été un fan de la méthode suivante assertions fluides .


Concernant

Tu abordes ça de la mauvaise façon. Testez simplement votre fonctionnalité : si une exception est levée, le test échouera automatiquement. Si aucune exception n'est levée, vos tests seront tous verts.

C'est correct en principe mais incorrect en conclusion.

Java autorise les exceptions pour le flux de contrôle. Ceci est réalisé par le runtime JRE lui-même dans des API telles que Double.parseDouble via un NumberFormatException y Paths.get via un InvalidPathException .

Étant donné que vous avez écrit un composant qui valide les chaînes de nombres pour Double.ParseDouble Comment tester au mieux ce composant, en utilisant peut-être une Regex, un analyseur écrit à la main, ou peut-être quelque chose qui intègre d'autres règles de domaine qui restreignent la plage d'un double à quelque chose de spécifique ? Je pense qu'un test évident serait d'affirmer que, lorsque la chaîne résultante est analysée, aucune exception n'est levée. J'écrirais ce test en utilisant l'une des méthodes suivantes assertDoesNotThrow o /*comment*/{code} bloc. Quelque chose comme

@Test public void given_validator_accepts_string_result_should_be_interpretable_by_doubleParseDouble(){
  //setup
  String input = "12.34E+26" //a string double with domain significance

  //act
  boolean isValid = component.validate(input)

  //assert -- using the library 'assertJ', my personal favourite 
  assertThat(isValid).describedAs(input + " was considered valid by component").isTrue();
  assertDoesNotThrow(() -> Double.parseDouble(input));
}

Je vous encourage également à paramétrer ce test sur input en utilisant Theories o Parameterized afin que vous puissiez réutiliser plus facilement ce test pour d'autres entrées. Sinon, si vous voulez faire dans l'exotisme, vous pouvez opter pour un test outil de génération de tests (et este ). TestNG a un meilleur support pour les tests paramétrés.

Ce que je trouve particulièrement désagréable, c'est la recommandation d'utiliser @Test(expectedException=IllegalArgumentException.class) , cette exception est dangereusement large . Si votre code est modifié de telle sorte que le constructeur du composant testé a été modifié if(constructorArgument <= 0) throw IllegalArgumentException() et que votre test a fourni 0 pour cet argument parce que c'était pratique - et c'est très courant, parce que la bonne génération de données de test est un problème étonnamment difficile -, alors votre test aura une barre verte même s'il ne teste rien. Un tel test est pire qu'inutile.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X