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

Je sais que l’une des façons 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"); } } 

Y a-t-il une manière plus propre de le faire? (Probablement en utilisant @Rule de Junit?)

Vous vous approchez de la mauvaise façon. Testez simplement vos fonctionnalités: si une exception est lancée, le test échouera automatiquement. Si aucune exception n’est levée, vos tests seront tous verts.

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

Contexte des tests unitaires

Lorsque vous effectuez des tests unitaires, il est important de définir vous-même ce que vous considérez comme une unité de travail. Fondamentalement: une extraction de votre base de code qui peut inclure ou non plusieurs méthodes ou classes représentant une seule fonctionnalité.

Ou, tel que défini dans The Art of Unit Testing, 2e édition par Roy Osherove , page 11:

Un test unitaire est un élément de code automatisé qui appelle l’unité de travail testée, puis vérifie certaines hypothèses concernant un résultat final unique de cette unité. Un test unitaire est presque toujours écrit en utilisant un framework de test unitaire. Il peut être écrit facilement et s’exécute rapidement. C’est fiable, lisible et maintenable. C’est cohérent dans ses résultats tant que le code de production n’a pas changé.

Ce qui est important à comprendre, c’est qu’une unité de travail n’est généralement pas une méthode mais au niveau de base, c’est une méthode et après elle est encapsulée par une autre unité de travail.

entrer la description de l'image ici

Idéalement, vous devriez avoir une méthode de test pour chaque unité de travail afin que vous puissiez toujours voir immédiatement où les choses vont mal. Dans cet exemple, il y a une méthode de base appelée getUserById() qui renvoie un utilisateur et il y a un total de 3 unités de travail.

La première unité de travail doit tester si un utilisateur valide est retourné ou non en cas de saisie valide et non valide.
Toutes les exceptions lancées par la source de données doivent être gérées ici: si aucun utilisateur n’est présent, il doit y avoir un test démontrant qu’une exception est levée lorsque l’utilisateur est introuvable. Un exemple de ceci pourrait être le IllegalArgumentException qui est intercepté avec l’ @Test(expected = IllegalArgumentException.class) .

Une fois que vous avez traité toutes vos utilisations pour cette unité de travail de base, vous passez à un niveau supérieur. Ici, vous faites exactement la même chose, mais vous ne gérez que les exceptions venant du niveau juste en dessous du niveau actuel. Cela maintient votre code de test bien structuré et vous permet de parcourir rapidement l’architecture pour trouver où les choses vont mal, au lieu d’avoir à sauter partout.

Manipulation d’une entrée valide et défectueuse des tests

À ce stade, il devrait être clair comment nous allons gérer ces exceptions. Il y a 2 types d’entrées: entrée valide et entrée défectueuse (l’entrée est valide au sens ssortingct, mais ce n’est pas correct).

Lorsque vous travaillez avec des entrées valides, vous définissez l’espérance implicite que, quel que soit le test que vous écrivez, cela fonctionnera.

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

En ajoutant un autre test ( nonExistingUserById_ShouldThrow_IllegalArgumentException ) qui utilise l’entrée défectueuse et attend une exception, vous pouvez voir si votre méthode fait ce qu’elle est censée faire avec des entrées incorrectes.

TL; DR

Vous essayiez de faire deux choses dans votre test: vérifiez les entrées valides et erronées. En divisant cela en deux méthodes qui font chacune une chose, vous obtiendrez des tests beaucoup plus clairs et une meilleure vue d’ensemble des problèmes.

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 les calques en dessous du calque actuel sont une garantie virtuelle que vos dépendances fonctionnent et si quelque chose ne va pas, c’est dans votre calque actuel (en supposant que les calques inférieurs ne jettent aucune erreur eux-mêmes).

Je suis tombé sur cela à cause de la règle SonarQubes “squid: S2699”: “Ajouter au moins une assertion à ce cas de test.”

J’ai eu un test simple que le seul but était, il est passé sans exception.

Imaginez ce code simple:

 public class Printer { public static void printLine(final Ssortingng line) { System.out.println(line); } } 

Quel type d’assertion peut-on append pour tester cette méthode? Bien sûr, vous pouvez essayer de le contourner, mais ce n’est que du code gonflant.

La solution vous donne JUnit lui-même.

Dans les cas où aucune exception n’est levée et que vous souhaitez illustrer explicitement ce comportement, ajoutez simplement le paramètre attendu comme suit:

 @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.

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 alors votre code devient simplement:

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

Si vous n’avez pas access à Java-8, j’utiliserais une installation java très ancienne: des blocs de code aribitrary et un simple commentaire

 //setup Component component = new Component(); //act configure(component); //assert /*assert does not throw*/{ component.doSomething(); } 

Et enfin, avec kotlin, une langue dont je suis récemment tombée amoureuse:

 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 jouer avec exactement ce que vous voulez exprimer, j’ai toujours été fan des affirmations courantes .


En ce qui concerne

Vous vous approchez de la mauvaise façon. Testez simplement vos fonctionnalités: si une exception est lancé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 des exceptions pour le stream de contrôle. Ceci est effectué par le moteur d’exécution JRE lui-même dans des API telles que Double.parseDouble via NumberFormatException et Paths.get via une InvalidPathException .

Étant donné que vous avez écrit un composant qui valide les chaînes de Double.ParseDouble pour Double.ParseDouble , peut-être en utilisant un Regex, peut-être un parsingur écrit à la main, ou peut-être quelque chose qui limite la scope d’un double à quelque chose de spécifique. tester ce composant? Je pense qu’un test évident consisterait à affirmer que, lorsque la chaîne résultante est analysée, aucune exception n’est levée. J’écrirais ce test en utilisant soit le assertDoesNotThrow ci-dessus assertDoesNotThrow ou /*comment*/{code} . Quelque chose comme

 @Test public void given_validator_accepts_ssortingng_result_should_be_interpretable_by_doubleParseDouble(){ //setup Ssortingng input = "12.34E+26" //a ssortingng 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 en input utilisant Theories ou Parameterized afin de pouvoir réutiliser plus facilement ce test pour d’autres entrées. Alternativement, si vous voulez être exotique, vous pouvez opter pour un outil de génération de test (et ceci ). TestNG supporte mieux 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 change de telle sorte que le constructeur du composant sous test a if(constructorArgument <= 0) throw IllegalArgumentException() , et votre test fournissait 0 pour cet argument car il était pratique - et cela est très courant, car une bonne génération de données de test est un problème étonnamment difficile--, alors votre test sera vert, même s'il ne teste rien. Un tel test est pire qu'inutile.

Si vous êtes assez malchanceux pour attraper toutes les erreurs dans votre code. Tu peux bêtement faire

 class DumpTest { Exception ex; @Test public void testWhatEver() { try { thisShouldThroughError(); } catch (Exception e) { ex = e; } assertEquals(null,ex); } } 

Avec assertions des affirmations courantes 3.7.0 :

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

JUnit5 ajoute la méthode assertAll () dans ce but précis.

 assertAll( () -> foo() ) 

source: API JUnit 5

Vous pouvez le faire en utilisant un @Rule et ensuite appeler la méthode reportMissingExceptionWithMessage comme indiqué ci-dessous: Ceci est Scala, mais cela peut facilement être fait de la même manière en Java.

entrer la description de l'image ici

Si vous souhaitez tester cela, votre cible de test consum l’exception. Il suffit de laisser le test en tant que (collaborateur simulé en utilisant jMock2):

 @Test public void consumesAndLogsExceptions() throws Exception { context.checking(new Expectations() { { oneOf(collaborator).doSth(); will(throwException(new NullPointerException())); } }); target.doSth(); } 

Le test passerait si votre cible consum l’exception levée, sinon le test échouerait.

Si vous souhaitez tester votre logique de consommation d’exception, les choses deviennent plus complexes. Je suggère de déléguer la consommation à un collaborateur qui pourrait se moquer. Par conséquent, le test pourrait être:

 @Test public void consumesAndLogsExceptions() throws Exception { Exception e = new NullPointerException(); context.checking(new Expectations() { { allowing(collaborator).doSth(); will(throwException(e)); oneOf(consumer).consume(e); } }); target.doSth(); } 

Mais parfois, il est sur-conçu si vous voulez simplement le connecter. Dans ce cas, cet article ( http://java.dzone.com/articles/monitoring-declarative-transac , http://blog.novoj.net/2008/09/20/testing-aspect-pointcuts-is-here -an-easy-way / ) peut aider si vous insistez sur tdd dans ce cas.

Utilisez assertNull (…)

 @Test public void foo() { try { //execute code that you expect not to throw Exceptions. } catch (Exception e){ assertNull(e); } } 

JUnit 5 (Jupiter) fournit trois fonctions pour vérifier l’absence / la présence d’une exception:

assertAll​()

Affirme que tous les executables fournis
ne jette pas d’exceptions.

assertDoesNotThrow​()

Affirme que l’exécution du
fourni executable / supplier
ne jette aucune sorte d’ exception .

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

assertThrows​()

Affirme que l’exécution de l’ executable fourni
lève une exception du type expectedType
et renvoie l’ 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_ssortingng_has_been_constructed_then_myFunction_does_not_throw() { Ssortingng mySsortingng = "this ssortingng has been constructed"; assertAll(() -> MyClass.myFunction(mySsortingng)); } @Test void when_ssortingng_has_been_constructed_then_myFunction_does_not_throw__junit_v520() { Ssortingng mySsortingng = "this ssortingng has been constructed"; assertDoesNotThrow(() -> MyClass.myFunction(mySsortingng)); } @Test void when_ssortingng_is_null_then_myFunction_throws_IllegalArgumentException() { Ssortingng mySsortingng = null; assertThrows( IllegalArgumentException.class, () -> MyClass.myFunction(mySsortingng)); } } 

Vous pouvez vous attendre à ce que cette exception ne soit pas créée en créant une règle.

 @Rule public ExpectedException expectedException = ExpectedException.none(); 

Le test suivant échoue pour toutes les exceptions, coché ou non:

 @Test public void testMyCode() { try { runMyTestCode(); } catch (Throwable t) { throw new Error("fail!"); } }