Comment pouvez-vous affirmer qu'une certaine exception est levée dans les tests JUnit 4?

2001

Comment puis-je utiliser JUnit4 idiomatiquement pour tester qu'un code lève une exception?

Bien que je puisse certainement faire quelque chose comme ça:

@Test
public void testFooThrowsIndexOutOfBoundsException() {
  boolean thrown = false;

  try {
    foo.doStuff();
  } catch (IndexOutOfBoundsException e) {
    thrown = true;
  }

  assertTrue(thrown);
}

Je me souviens qu'il y a une annotation ou un Assert.xyz ou quelque chose qui est beaucoup moins délicat et beaucoup plus dans l'esprit de JUnit pour ce genre de situations.

SCdF
la source
21
Le problème avec toute autre approche, mais c'est qu'ils terminent invariablement le test une fois l'exception levée. D'un autre côté, je veux souvent encore appeler org.mockito.Mockito.verifyavec divers paramètres pour m'assurer que certaines choses se sont produites (telles qu'un service d'enregistrement a été appelé avec les paramètres corrects) avant que l'exception ne soit levée.
ZeroOne
5
Vous pouvez voir comment tester les exceptions dans la page wiki JUnit github.com/junit-team/junit/wiki/Exception-testing
PhoneixS
6
@ZeroOne - Pour cela, j'aurais deux tests différents - un pour l'exception et un pour vérifier l'interaction avec votre maquette.
tddmonkey
Il existe un moyen de le faire avec JUnit 5, j'ai mis à jour ma réponse ci-dessous.
Dilini Rajapaksha

Réponses:

2363

Cela dépend de la version de JUnit et des bibliothèques d'assert que vous utilisez.

La réponse originale JUnit <= 4.12était:

@Test(expected = IndexOutOfBoundsException.class)
public void testIndexOutOfBoundsException() {

    ArrayList emptyList = new ArrayList();
    Object o = emptyList.get(0);

}

Bien que la réponse https://stackoverflow.com/a/31826781/2986984 ait plus d'options pour JUnit <= 4.12.

Référence :

skaffman
la source
66
Ce morceau de code ne fonctionnera pas si vous vous attendez à une exception uniquement quelque part dans votre code, et non à une couverture comme celle-ci.
Oh Chin Boon
4
@skaffman Cela ne fonctionnerait pas avec org.junit.experimental.theories.Theory dirigé par org.junit.experimental.theories.Theories
Artem Oboturov
74
Roy Osherove décourage ce type de test d'exception dans L'art du test unitaire , car l'exception peut être n'importe où à l'intérieur du test et pas seulement à l'intérieur de l'unité testée.
Kevin Wittek
21
Je suis en désaccord avec @ Kiview / Roy Osherove. À mon avis, les tests devraient porter sur le comportement et non sur la mise en œuvre. En testant qu'une méthode spécifique peut générer une erreur, vous liez directement vos tests à l'implémentation. Je dirais que tester dans la méthode indiquée ci-dessus fournit un test plus précieux. La mise en garde que j'ajouterais est que dans ce cas, je testerais une exception personnalisée, de sorte que je sais que j'obtiens l'exception que je veux vraiment.
nickbdyer
6
Ni. Je veux tester le comportement de la classe. Ce qui est important, c'est que si j'essaie de récupérer quelque chose qui n'est pas là, j'obtiens une exception. Le fait que la structure de données ArrayListréponde get()n'est pas pertinent. Si je choisissais à l'avenir de passer à un tableau primitif, je devrais alors changer cette implémentation de test. La structure des données doit être masquée, afin que le test puisse se concentrer sur le comportement de la classe .
nickbdyer
1317

Edit: Maintenant que JUnit 5 et JUnit 4.13 sont sortis, la meilleure option serait d'utiliser Assertions.assertThrows() (pour JUnit 5) et Assert.assertThrows()(pour JUnit 4.13). Voir mon autre réponse pour plus de détails.

Si vous n'avez pas migré vers JUnit 5, mais pouvez utiliser JUnit 4.7, vous pouvez utiliser la ExpectedExceptionrègle:

public class FooTest {
  @Rule
  public final ExpectedException exception = ExpectedException.none();

  @Test
  public void doStuffThrowsIndexOutOfBoundsException() {
    Foo foo = new Foo();

    exception.expect(IndexOutOfBoundsException.class);
    foo.doStuff();
  }
}

C'est bien mieux que @Test(expected=IndexOutOfBoundsException.class)parce que le test échouera s'il IndexOutOfBoundsExceptionest lancé avantfoo.doStuff()

Voir cet article pour plus de détails

NamshubWriter
la source
14
@skaffman - Si j'ai bien compris, il semble que l'exception.expect ne soit appliquée que dans un seul test, pas dans toute la classe.
bacar
5
Si l'exception que nous pensons être levée est une exception vérifiée, devrions-nous ajouter des lancers ou essayer de rattraper ou tester cette situation d'une autre manière?
Mohammad Jafar Mashhadi
5
@MartinTrummer Aucun code ne doit s'exécuter après foo.doStuff () car l'exception est levée et la méthode est fermée. Avoir du code après une exception attendue (à l'exception de la fermeture des ressources dans un enfin) est de toute façon inutile car il ne devrait jamais être exécuté si l'exception est levée.
Jason Thompson
9
C'est la meilleure approche. Il y a ici deux avantages par rapport à la solution de skaffman. Premièrement, la ExpectedExceptionclasse a des moyens de faire correspondre le message de l'exception, ou même d'écrire votre propre correspondance qui dépend de la classe de l'exception. Deuxièmement, vous pouvez définir votre attente immédiatement avant la ligne de code que vous prévoyez de lever l'exception - ce qui signifie que votre test échouera si la mauvaise ligne de code lève l'exception; alors qu'il n'y a aucun moyen de le faire avec la solution de skaffman.
Dawood ibn Kareem du
5
@MJafarMash si l'exception que vous prévoyez de lever est vérifiée, vous ajouterez cette exception à la clause throws de la méthode de test. Vous faites de même chaque fois que vous testez une méthode déclarée pour lever une exception vérifiée, même si l'exception n'est pas déclenchée dans le cas de test particulier.
NamshubWriter
472

Soyez prudent en utilisant l'exception attendue, car elle affirme uniquement que la méthode a levé cette exception, pas une ligne de code particulière dans le test.

J'ai tendance à l'utiliser pour tester la validation des paramètres, car ces méthodes sont généralement très simples, mais les tests plus complexes pourraient être mieux servis avec:

try {
    methodThatShouldThrow();
    fail( "My method didn't throw when I expected it to" );
} catch (MyException expectedException) {
}

Faites preuve de jugement.

daveb
la source
95
Je suis peut-être vieille école mais je préfère toujours ça Cela me donne également un endroit pour tester l'exception elle-même: parfois j'ai des exceptions avec des getters pour certaines valeurs, ou je peux simplement chercher une valeur particulière dans le message (par exemple en recherchant "xyz" dans le message "code non reconnu 'xyz' ").
Rodney Gitzel
3
Je pense que l'approche de NamshubWriter vous offre le meilleur des deux mondes.
Eddie
4
En utilisant ExpectedException, vous pouvez appeler N exception.expect par méthode pour tester comme cette exception.expect (IndexOutOfBoundsException.class); foo.doStuff1 (); exception.expect (IndexOutOfBoundsException.class); foo.doStuff2 (); exception.expect (IndexOutOfBoundsException.class); foo.doStuff3 ();
user1154664
10
@ user1154664 En fait, vous ne pouvez pas. En utilisant ExpectedException, vous pouvez uniquement tester qu'une méthode lève une exception, car lorsque cette méthode est appelée, le test s'arrête de s'exécuter car elle a levé l'exception attendue!
NamshubWriter
2
Votre première phrase n'est tout simplement pas vraie. Lors de l'utilisation ExpectedException, la chose normale à faire est de définir l'attente immédiatement avant la ligne à laquelle vous prévoyez de lever l'exception. De cette façon, si une ligne antérieure lève l'exception, elle ne déclenchera pas la règle et le test échouera.
Dawood ibn Kareem
213

Comme indiqué précédemment, il existe de nombreuses façons de gérer les exceptions dans JUnit. Mais avec Java 8, il y en a un autre: utiliser les expressions Lambda. Avec les expressions Lambda, nous pouvons obtenir une syntaxe comme celle-ci:

@Test
public void verifiesTypeAndMessage() {
    assertThrown(new DummyService()::someMethod)
            .isInstanceOf(RuntimeException.class)
            .hasMessage("Runtime exception occurred")
            .hasMessageStartingWith("Runtime")
            .hasMessageEndingWith("occurred")
            .hasMessageContaining("exception")
            .hasNoCause();
}

assertThrown accepte une interface fonctionnelle, dont les instances peuvent être créées avec des expressions lambda, des références de méthode ou des références de constructeur. assertThrown acceptant cette interface attendra et sera prêt à gérer une exception.

Il s'agit d'une technique relativement simple mais puissante.

Jetez un œil à cet article de blog décrivant cette technique: http://blog.codeleak.pl/2014/07/junit-testing-exception-with-java-8-and-lambda-expressions.html

Le code source peut être trouvé ici: https://github.com/kolorobot/unit-testing-demo/tree/master/src/test/java/com/github/kolorobot/exceptions/java8

Divulgation: je suis l'auteur du blog et du projet.

Rafal Borowiec
la source
2
J'aime cette solution, mais puis-je la télécharger à partir d'un dépôt Maven?
Selwyn
@Airduster une implémentation de cette idée disponible sur Maven est stefanbirkner.github.io/vallado
NamshubWriter
6
@CristianoFontes une version plus simple de cette API est prévue pour JUnit 4.13. Voir github.com/junit-team/junit/commit/…
NamshubWriter
@RafalBorowiec est techniquement new DummyService()::someMethodun MethodHandle, mais cette approche fonctionne aussi bien avec les expressions lambda.
Andy
@NamshubWriter, il semble que le junit 4.13 a été abandonné au profit du junit 5: stackoverflow.com/questions/156503/…
Vadzim
154

dans junit, il existe quatre façons de tester l'exception.

junit5.x

  • pour junit5.x, vous pouvez utiliser assertThrowscomme suit

    @Test
    public void testFooThrowsIndexOutOfBoundsException() {
        Throwable exception = assertThrows(IndexOutOfBoundsException.class, () -> foo.doStuff());
        assertEquals("expected messages", exception.getMessage());
    }

junit4.x

  • pour junit4.x, utilisez l'attribut facultatif «attendu» de Test annonation

    @Test(expected = IndexOutOfBoundsException.class)
    public void testFooThrowsIndexOutOfBoundsException() {
        foo.doStuff();
    }
  • pour junit4.x, utilisez la règle ExpectedException

    public class XxxTest {
        @Rule
        public ExpectedException thrown = ExpectedException.none();
    
        @Test
        public void testFooThrowsIndexOutOfBoundsException() {
            thrown.expect(IndexOutOfBoundsException.class)
            //you can test the exception message like
            thrown.expectMessage("expected messages");
            foo.doStuff();
        }
    }
  • vous pouvez également utiliser la méthode classique try / catch largement utilisée dans le cadre de junit 3

    @Test
    public void testFooThrowsIndexOutOfBoundsException() {
        try {
            foo.doStuff();
            fail("expected exception was not occured.");
        } catch(IndexOutOfBoundsException e) {
            //if execution reaches here, 
            //it indicates this exception was occured.
            //so we need not handle it.
        }
    }
  • donc

    • si vous aimez le 5 juin, alors vous devriez aimer le 1er
    • la 2ème voie est utilisée lorsque vous ne souhaitez tester que le type d'exception
    • les deux premiers et les derniers sont utilisés lorsque vous souhaitez tester davantage le message d'exception
    • si vous utilisez junit 3, alors le 4ème est préféré
  • pour plus d'informations, vous pouvez lire ce document et le guide de l'utilisateur junit5 pour plus de détails.

walsh
la source
6
Pour moi, c'est la meilleure réponse, elle couvre très clairement toutes les voies, merci! Personnellement, je continue à utiliser la 3e option même avec Junit4 pour plus de lisibilité, pour éviter le bloc de capture vide, vous pouvez également attraper Throwable et affirmer le type d'e
Nicolas Cornette
Est-il possible d'utiliser ExpectedException pour s'attendre à une exception vérifiée?
miuser
Il ne s'agit que d'une accumulation des trois premières réponses. OMI, cette réponse n'aurait même pas dû être publiée si elle n'ajoute rien de nouveau. Il suffit de répondre (une question populaire) au représentant. Assez inutile.
Paul Samsotha
bien sûr, car vous pouvez passer n'importe quel type dérivé de Trowableà la méthode ExpectedException.expect. veuillez voir sa signature . @miuser
walsh
116

tl; dr

  • post-JDK8: utilisez AssertJ ou lambdas personnalisés pour affirmer un comportement exceptionnel .

  • pré-JDK8: Je vais recommander le bon vieux try- catchbloc. ( N'oubliez pas d'ajouter une fail()assertion avant le catchbloc )

Indépendamment de Junit 4 ou JUnit 5.

la longue histoire

Il est possible de vous écrire vous -même try - catchbloquer ou utiliser les outils JUnit ( @Test(expected = ...)ou la @Rule ExpectedExceptionfonction de règle JUnit).

Mais ces moyens ne sont pas si élégant et ne se mélangent pas bien sage lisibilité avec d' autres outils. De plus, l'outillage JUnit présente certains pièges.

  1. Le bloc try- catchvous devez écrire le bloc autour du comportement testé et écrire l'assertion dans le bloc catch, cela peut être correct, mais beaucoup trouvent que ce style interrompt le flux de lecture d'un test. De plus, vous devez écrire un Assert.failà la fin du trybloc. Sinon, le test peut manquer un côté des assertions; PMD , findbugs ou Sonar détecteront ces problèmes.

  2. La @Test(expected = ...)fonctionnalité est intéressante car vous pouvez écrire moins de code, puis écrire ce test est censé être moins sujet aux erreurs de codage. Mais cette approche fait défaut dans certains domaines.

    • Si le test doit vérifier des éléments supplémentaires sur l'exception comme la cause ou le message (de bons messages d'exception sont vraiment importants, avoir un type d'exception précis peut ne pas suffire).
    • De plus, comme l'attente est placée dans la méthode, selon la façon dont le code testé est écrit, la mauvaise partie du code de test peut lever l'exception, conduisant à un test faussement positif et je ne suis pas sûr que PMD , findbugs ou Sonar donnera des conseils sur ce code.

      @Test(expected = WantedException.class)
      public void call2_should_throw_a_WantedException__not_call1() {
          // init tested
          tested.call1(); // may throw a WantedException
      
          // call to be actually tested
          tested.call2(); // the call that is supposed to raise an exception
      }
  3. La ExpectedExceptionrègle est également une tentative de corriger les mises en garde précédentes, mais cela semble un peu gênant à utiliser car il utilise un style d'attente, les utilisateurs d' EasyMock connaissent très bien ce style. Cela peut être pratique pour certains, mais si vous suivez les principes de développement conduit par le comportement (BDD) ou d' Arrange Act Assert (AAA), la ExpectedExceptionrègle ne rentrera pas dans ces styles d'écriture. En dehors de cela, il peut souffrir du même problème que le @Testchemin, selon l'endroit où vous placez l'attente.

    @Rule ExpectedException thrown = ExpectedException.none()
    
    @Test
    public void call2_should_throw_a_WantedException__not_call1() {
        // expectations
        thrown.expect(WantedException.class);
        thrown.expectMessage("boom");
    
        // init tested
        tested.call1(); // may throw a WantedException
    
        // call to be actually tested
        tested.call2(); // the call that is supposed to raise an exception
    }

    Même l'exception attendue est placée avant l'instruction de test, elle rompt votre flux de lecture si les tests suivent BDD ou AAA.

    Voir également ce numéro de commentaire sur JUnit de l'auteur de ExpectedException. JUnit 4.13-beta-2 déconseille même ce mécanisme:

    Tirez la demande # 1519 : Deprecate ExpectedException

    La méthode Assert.assertThrows fournit une meilleure façon de vérifier les exceptions. De plus, l'utilisation d'ExpectedException est sujette aux erreurs lorsqu'elle est utilisée avec d'autres règles comme TestWatcher car l'ordre des règles est important dans ce cas.

Ces options ci-dessus ont donc toute leur charge de mises en garde et ne sont clairement pas à l'abri des erreurs de codeur.

  1. Il y a un projet dont j'ai pris connaissance après avoir créé cette réponse qui semble prometteur, c'est une exception de capture .

    Comme le dit la description du projet, il a permis à un codeur d'écrire dans une ligne de code fluide interceptant l'exception et de proposer cette exception pour la dernière assertion. Et vous pouvez utiliser n'importe quelle bibliothèque d'assertions comme Hamcrest ou AssertJ .

    Un exemple rapide tiré de la page d'accueil:

    // given: an empty list
    List myList = new ArrayList();
    
    // when: we try to get the first element of the list
    when(myList).get(1);
    
    // then: we expect an IndexOutOfBoundsException
    then(caughtException())
            .isInstanceOf(IndexOutOfBoundsException.class)
            .hasMessage("Index: 1, Size: 0") 
            .hasNoCause();

    Comme vous pouvez voir que le code est vraiment simple, vous interceptez l'exception sur une ligne spécifique, l' thenAPI est un alias qui utilisera les API AssertJ (similaire à l'utilisation assertThat(ex).hasNoCause()...). À un moment donné, le projet s'est appuyé sur FEST-Assert, l'ancêtre d'AssertJ . EDIT: Il semble que le projet prépare un support Lambdas Java 8.

    Actuellement, cette bibliothèque présente deux lacunes:

    • Au moment d'écrire ces lignes, il est à noter que cette bibliothèque est basée sur Mockito 1.x car elle crée une maquette de l'objet testé derrière la scène. Comme Mockito n'est toujours pas mis à jour, cette bibliothèque ne peut pas fonctionner avec les classes finales ou les méthodes finales . Et même s'il était basé sur Mockito 2 dans la version actuelle, cela nécessiterait de déclarer un fabricant de maquette global ( inline-mock-maker), ce qui peut ne pas être ce que vous voulez, car ce fabricant de maquette a différents inconvénients que le fabricant de maquette classique.

    • Cela nécessite encore une autre dépendance de test.

    Ces problèmes ne s'appliqueront pas une fois que la bibliothèque prend en charge les lambdas. Cependant, la fonctionnalité sera dupliquée par le jeu d'outils AssertJ.

    Tenant compte de tout si vous ne voulez pas utiliser l'outil catch-exception, je recommanderai l'ancienne bonne façon du bloc try- catch, du moins jusqu'au JDK7. Et pour les utilisateurs de JDK 8, vous préférerez peut-être utiliser AssertJ car il offre plus que la simple affirmation d'exceptions.

  2. Avec le JDK8, les lambdas entrent en scène de test, et ils se sont révélés être un moyen intéressant d'affirmer un comportement exceptionnel. AssertJ a été mis à jour pour fournir une belle API fluide pour affirmer un comportement exceptionnel.

    Et un exemple de test avec AssertJ :

    @Test
    public void test_exception_approach_1() {
        ...
        assertThatExceptionOfType(IOException.class)
                .isThrownBy(() -> someBadIOOperation())
                .withMessage("boom!"); 
    }
    
    @Test
    public void test_exception_approach_2() {
        ...
        assertThatThrownBy(() -> someBadIOOperation())
                .isInstanceOf(Exception.class)
                .hasMessageContaining("boom");
    }
    
    @Test
    public void test_exception_approach_3() {
        ...
        // when
        Throwable thrown = catchThrowable(() -> someBadIOOperation());
    
        // then
        assertThat(thrown).isInstanceOf(Exception.class)
                          .hasMessageContaining("boom");
    }
  3. Avec une réécriture presque complète de JUnit 5, les assertions ont été un peu améliorées , elles peuvent s'avérer intéressantes comme une méthode prête à l'emploi pour affirmer correctement une exception. Mais vraiment l'API d'assertion est encore un peu pauvre, il n'y a rien à l'extérieur assertThrows.

    @Test
    @DisplayName("throws EmptyStackException when peeked")
    void throwsExceptionWhenPeeked() {
        Throwable t = assertThrows(EmptyStackException.class, () -> stack.peek());
    
        Assertions.assertEquals("...", t.getMessage());
    }

    Comme vous l'avez remarqué, assertEqualsrevient toujours void, et en tant que tel ne permet pas de chaîner des assertions comme AssertJ.

    Aussi, si vous vous souvenez d'un conflit de noms avec Matcherou Assert, soyez prêt à rencontrer le même conflit avec Assertions.

Je voudrais conclure qu'aujourd'hui (2017-03-03) la facilité d'utilisation d' AssertJ , l'API découvrable, le rythme rapide de développement et en tant que dépendance de facto aux tests est la meilleure solution avec JDK8 quel que soit le cadre de test (JUnit ou non), les JDK antérieurs devraient plutôt compter sur try-catch blocs même s'ils se sentent maladroits.

Cette réponse a été copiée d' une autre question qui n'a pas la même visibilité, je suis le même auteur.

Brice
la source
1
Ajouter org.junit.jupiter: junit-jupiter-engine: 5.0.0-RC2 dependency (en plus du déjà existant junit: junit: 4.12) pour pouvoir utiliser assertThrows n'est peut-être pas la solution préférée, mais n'a provoqué aucune problèmes pour moi.
anre
Je suis un fan de l'utilisation de la règle ExpectedException, mais cela m'a toujours dérangé que cela rompt avec AAA. Vous avez écrit un excellent article pour décrire toutes les différentes approches et vous m'avez certainement encouragé à essayer AssertJ :-) Merci!
Pim Hazebroek
@PimHazebroek merci. L'API AssertJ est assez riche. Mieux à mon avis que ce que JUnit propose hors de la boîte.
Brice
64

Maintenant que JUnit 5 et JUnit 4.13 sont sortis, la meilleure option serait d'utiliser Assertions.assertThrows() (pour JUnit 5) et Assert.assertThrows()(pour JUnit 4.13). Voir le Guide de l'utilisateur de Junit 5 .

Voici un exemple qui vérifie qu'une exception est levée et utilise la vérité pour faire des assertions sur le message d'exception:

public class FooTest {
  @Test
  public void doStuffThrowsIndexOutOfBoundsException() {
    Foo foo = new Foo();

    IndexOutOfBoundsException e = assertThrows(
        IndexOutOfBoundsException.class, foo::doStuff);

    assertThat(e).hasMessageThat().contains("woops!");
  }
}

Les avantages par rapport aux approches des autres réponses sont:

  1. Construit dans JUnit
  2. Vous obtenez un message d'exception utile si le code dans le lambda ne lève pas d'exception, et un stacktrace s'il lève une exception différente
  3. Concis
  4. Permet à vos tests de suivre Arrange-Act-Assert
  5. Vous pouvez indiquer précisément quel code vous prévoyez de lever l'exception
  6. Vous n'avez pas besoin de répertorier l'exception attendue dans la throwsclause
  7. Vous pouvez utiliser le cadre d'assertion de votre choix pour faire des affirmations sur l'exception interceptée

Une méthode similaire sera ajoutée à org.junit AssertJUnit 4.13.

NamshubWriter
la source
Cette approche est propre, mais je ne vois pas comment cela permet à notre test de suivre "Arrange-Act-Assert", car nous devons encapsuler la partie "Act" dans un "assertThrow", qui est une assert.
Clockwork
@Clockwork La lambda est "l'acte". Le but d'Arrange-Act-Assert est de rendre le code propre et simple (et donc facile à comprendre et à maintenir). Comme vous l'avez dit, cette approche est propre.
NamshubWriter
J'espérais toujours pouvoir affirmer le lancer et l'exception à la fin du test, dans la partie "assert". Dans cette approche, vous devez envelopper l'acte dans une première affirmation pour l'attraper en premier.
Clockwork
Cela nécessiterait plus de code dans chaque test pour faire l'assertion. C'est plus de code et serait sujet aux erreurs.
NamshubWriter
43

Que diriez-vous de cela: attrapez une exception très générale, assurez-vous qu'elle sort du bloc catch, puis affirmez que la classe de l'exception est ce que vous attendez d'elle. Cette affirmation échouera si a) l'exception est du mauvais type (par exemple, si vous avez obtenu un pointeur nul à la place) et b) l'exception n'a jamais été levée.

public void testFooThrowsIndexOutOfBoundsException() {
  Throwable e = null;

  try {
    foo.doStuff();
  } catch (Throwable ex) {
    e = ex;
  }

  assertTrue(e instanceof IndexOutOfBoundsException);
}
Johan
la source
3
De plus, vous ne verrez pas le type d'exception dans les résultats du test le jour où le test échoue.
jontejj
Cela peut être amélioré un peu en modifiant la façon dont vous affirmez à la fin. assertEquals(ExpectedException.class, e.getClass())vous montrera les valeurs attendues et réelles lorsque le test échoue.
Cypher
37

Solution de style BDD : JUnit 4 + Catch Exception + AssertJ

import static com.googlecode.catchexception.apis.BDDCatchException.*;

@Test
public void testFooThrowsIndexOutOfBoundsException() {

    when(() -> foo.doStuff());

    then(caughtException()).isInstanceOf(IndexOutOfBoundsException.class);

}

Dépendances

eu.codearte.catch-exception:catch-exception:2.0
MariuszS
la source
36

Utilisation d'une assertion AssertJ , qui peut être utilisée avec JUnit:

import static org.assertj.core.api.Assertions.*;

@Test
public void testFooThrowsIndexOutOfBoundsException() {
  Foo foo = new Foo();

  assertThatThrownBy(() -> foo.doStuff())
        .isInstanceOf(IndexOutOfBoundsException.class);
}

C'est mieux que @Test(expected=IndexOutOfBoundsException.class)parce qu'il garantit que la ligne attendue dans le test a levé l'exception et vous permet de vérifier plus de détails sur l'exception, comme le message, plus facilement:

assertThatThrownBy(() ->
       {
         throw new Exception("boom!");
       })
    .isInstanceOf(Exception.class)
    .hasMessageContaining("boom");

Instructions Maven / Gradle ici.

Weston
la source
manière la plus concise et personne ne l'apprécie, étrange .. Je n'ai qu'un seul problème avec la bibliothèque assertJ, assertThat entre en conflit avec Junit. en savoir plus sur assertJ throwby: JUnit: test d'exceptions avec Java 8 et AssertJ 3.0.0 ~ Codeleak.pl
ycomp
@ycomp Eh bien, c'est une nouvelle réponse à une très vieille question, donc la différence de score est trompeuse.
Weston
C'est probablement la meilleure solution si l'on peut utiliser Java 8 et AssertJ!
Pierre Henry
@ycomp Je soupçonne que ce conflit de nom peut être intentionnel: la bibliothèque AssertJ vous encourage donc fortement à ne jamais utiliser JUnit assertThat, toujours celle AssertJ. De plus, la méthode JUnit renvoie uniquement un type "normal", tandis que la méthode AssertJ renvoie une AbstractAssertsous-classe ... permettant la chaîne de méthodes comme ci-dessus (ou quels que soient les termes techniques pour cela ...).
mike rodent
@weston en fait, je viens d'utiliser votre technique dans AssertJ 2.0.0. Aucune excuse pour ne pas mettre à jour, sans aucun doute, mais vous voudrez peut-être le savoir.
mike rodent
33

Pour résoudre le même problème, j'ai mis en place un petit projet: http://code.google.com/p/catch-exception/

En utilisant ce petit assistant, vous écririez

verifyException(foo, IndexOutOfBoundsException.class).doStuff();

Ceci est moins détaillé que la règle ExpectedException de JUnit 4.7. Par rapport à la solution fournie par skaffman, vous pouvez spécifier dans quelle ligne de code vous attendez l'exception. J'espère que ça aide.

rwitzel
la source
J'ai pensé à faire quelque chose comme ça également, mais j'ai finalement découvert que la véritable puissance d'ExceptionException est que non seulement vous pouvez spécifier l'exception attendue, mais vous pouvez également spécifier certaines propriétés de l'exception telles que la cause attendue ou le message attendu.
Jason Thompson
Je suppose que cette solution présente certains des mêmes inconvénients que les simulacres? Par exemple, si fooest finalelle échouera parce que vous ne pouvez pas proxy foo?
Tom
Tom, si doStuff () fait partie d'une interface, l'approche proxy fonctionnera. Sinon, cette approche échouera, vous avez raison.
rwitzel
31

Mise à jour: JUnit5 a une amélioration des exceptions d' essai: assertThrows.

l'exemple suivant provient de: Junit 5 User Guide

 @Test
void exceptionTesting() {
    Throwable exception = assertThrows(IllegalArgumentException.class, () -> 
    {
        throw new IllegalArgumentException("a message");
    });
    assertEquals("a message", exception.getMessage());
}

Réponse originale en utilisant JUnit 4.

Il existe plusieurs façons de tester qu'une exception est levée. J'ai également discuté des options ci-dessous dans mon article Comment écrire d'excellents tests unitaires avec JUnit

Définissez le expectedparamètre @Test(expected = FileNotFoundException.class).

@Test(expected = FileNotFoundException.class) 
public void testReadFile() { 
    myClass.readFile("test.txt");
}

En utilisant try catch

public void testReadFile() { 
    try {
        myClass.readFile("test.txt");
        fail("Expected a FileNotFoundException to be thrown");
    } catch (FileNotFoundException e) {
        assertThat(e.getMessage(), is("The file test.txt does not exist!"));
    }

}

Test avec ExpectedExceptionrègle.

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

@Test
public void testReadFile() throws FileNotFoundException {

    thrown.expect(FileNotFoundException.class);
    thrown.expectMessage(startsWith("The file test.txt"));
    myClass.readFile("test.txt");
}

Vous pouvez en savoir plus sur les tests d'exceptions dans le wiki JUnit4 pour les tests d'exceptions et bad.robot - Expecting Exceptions JUnit Rule .

Dilini Rajapaksha
la source
22

Vous pouvez également le faire:

@Test
public void testFooThrowsIndexOutOfBoundsException() {
    try {
        foo.doStuff();
        assert false;
    } catch (IndexOutOfBoundsException e) {
        assert true;
    }
}
John Mikic
la source
12
Dans les tests JUnit, il est préférable d'utiliser Assert.fail(), non assert, juste au cas où vos tests s'exécuteraient dans un environnement où les assertions ne sont pas activées.
NamshubWriter
14

À mon humble avis, la meilleure façon de vérifier les exceptions dans JUnit est le modèle try / catch / fail / assert:

// this try block should be as small as possible,
// as you want to make sure you only catch exceptions from your code
try {
    sut.doThing();
    fail(); // fail if this does not throw any exception
} catch(MyException e) { // only catch the exception you expect,
                         // otherwise you may catch an exception for a dependency unexpectedly
    // a strong assertion on the message, 
    // in case the exception comes from anywhere an unexpected line of code,
    // especially important if your checking IllegalArgumentExceptions
    assertEquals("the message I get", e.getMessage()); 
}

Cela assertTruepourrait être un peu fort pour certaines personnes, donc cela assertThat(e.getMessage(), containsString("the message");pourrait être préférable.

Alex Collins
la source
13

La réponse la plus flexible et élégante pour Junit 4 que j'ai trouvée sur le blog Mkyong . Il a la flexibilité d' try/catchutiliser l' @Ruleannotation. J'aime cette approche car vous pouvez lire les attributs spécifiques d'une exception personnalisée.

package com.mkyong;

import com.mkyong.examples.CustomerService;
import com.mkyong.examples.exception.NameNotFoundException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.hasProperty;

public class Exception3Test {

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

    @Test
    public void testNameNotFoundException() throws NameNotFoundException {

        //test specific type of exception
        thrown.expect(NameNotFoundException.class);

        //test message
        thrown.expectMessage(is("Name is empty!"));

        //test detail
        thrown.expect(hasProperty("errCode"));  //make sure getters n setters are defined.
        thrown.expect(hasProperty("errCode", is(666)));

        CustomerService cust = new CustomerService();
        cust.findByName("");

    }

}
Dherik
la source
12

J'ai essayé de nombreuses méthodes ici, mais elles étaient soit compliquées, soit ne répondaient pas tout à fait à mes exigences. En fait, on peut tout simplement écrire une méthode d'aide:

public class ExceptionAssertions {
    public static void assertException(BlastContainer blastContainer ) {
        boolean caughtException = false;
        try {
            blastContainer.test();
        } catch( Exception e ) {
            caughtException = true;
        }
        if( !caughtException ) {
            throw new AssertionFailedError("exception expected to be thrown, but was not");
        }
    }
    public static interface BlastContainer {
        public void test() throws Exception;
    }
}

Utilisez-le comme ceci:

assertException(new BlastContainer() {
    @Override
    public void test() throws Exception {
        doSomethingThatShouldExceptHere();
    }
});

Zéro dépendances: pas besoin de mockito, pas besoin de powermock; et fonctionne très bien avec les classes finales.

Hugh Perkins
la source
Intéressant, mais ne convient pas à AAA (Arrange Act Assert), où vous voulez faire l'acte et l'étape Assert en différentes étapes.
bln-tom
1
@ bln-tom Techniquement, ce sont deux étapes différentes, elles ne sont tout simplement pas dans cet ordre. ; p
Trejkaz
10

Solution Java 8

Si vous souhaitez une solution qui:

  • Utilise des lambdas Java 8
  • Est -ce pas dépendre d'aucune magie JUnit
  • Vous permet de vérifier plusieurs exceptions dans une seule méthode de test
  • Recherche une exception levée par un ensemble spécifique de lignes dans votre méthode de test au lieu de toute ligne inconnue dans la méthode de test entière
  • Donne l'objet d'exception réel qui a été levé pour que vous puissiez l'examiner plus en détail

Voici une fonction utilitaire que j'ai écrite:

public final <T extends Throwable> T expectException( Class<T> exceptionClass, Runnable runnable )
{
    try
    {
        runnable.run();
    }
    catch( Throwable throwable )
    {
        if( throwable instanceof AssertionError && throwable.getCause() != null )
            throwable = throwable.getCause(); //allows "assert x != null : new IllegalArgumentException();"
        assert exceptionClass.isInstance( throwable ) : throwable; //exception of the wrong kind was thrown.
        assert throwable.getClass() == exceptionClass : throwable; //exception thrown was a subclass, but not the exact class, expected.
        @SuppressWarnings( "unchecked" )
        T result = (T)throwable;
        return result;
    }
    assert false; //expected exception was not thrown.
    return null; //to keep the compiler happy.
}

( extrait de mon blog )

Utilisez-le comme suit:

@Test
public void testThrows()
{
    RuntimeException e = expectException( RuntimeException.class, () -> 
        {
            throw new RuntimeException( "fail!" );
        } );
    assert e.getMessage().equals( "fail!" );
}
Mike Nakis
la source
8

Dans mon cas, je reçois toujours RuntimeException de db, mais les messages diffèrent. Et l'exception doit être gérée respectivement. Voici comment je l'ai testé:

@Test
public void testThrowsExceptionWhenWrongSku() {

    // Given
    String articleSimpleSku = "999-999";
    int amountOfTransactions = 1;
    Exception exception = null;

    // When
    try {
        createNInboundTransactionsForSku(amountOfTransactions, articleSimpleSku);
    } catch (RuntimeException e) {
        exception = e;
    }

    // Then
    shouldValidateThrowsExceptionWithMessage(exception, MESSAGE_NON_EXISTENT_SKU);
}

private void shouldValidateThrowsExceptionWithMessage(final Exception e, final String message) {
    assertNotNull(e);
    assertTrue(e.getMessage().contains(message));
}
Macchiatow
la source
1
avant la ligne avec } catch (, vous devez insérerfail("no exception thrown");
Daniel Alder
6

Créez simplement un Matcher qui peut être activé et désactivé, comme ceci:

public class ExceptionMatcher extends BaseMatcher<Throwable> {
    private boolean active = true;
    private Class<? extends Throwable> throwable;

    public ExceptionMatcher(Class<? extends Throwable> throwable) {
        this.throwable = throwable;
    }

    public void on() {
        this.active = true;
    }

    public void off() {
        this.active = false;
    }

    @Override
    public boolean matches(Object object) {
        return active && throwable.isAssignableFrom(object.getClass());
    }

    @Override
    public void describeTo(Description description) {
        description.appendText("not the covered exception type");
    }
}

Pour l'utiliser:

ajoutez public ExpectedException exception = ExpectedException.none();, puis:

ExceptionMatcher exMatch = new ExceptionMatcher(MyException.class);
exception.expect(exMatch);
someObject.somethingThatThrowsMyException();
exMatch.off();
Tor P
la source
6

Dans JUnit 4 ou version ultérieure, vous pouvez tester les exceptions comme suit

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


cela fournit de nombreuses fonctionnalités qui peuvent être utilisées pour améliorer nos tests JUnit.
Si vous voyez l'exemple ci-dessous, je teste 3 choses sur l'exception.

  1. Le type d'exception levée
  2. Le message d'exception
  3. La cause de l'exception


public class MyTest {

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

    ClassUnderTest classUnderTest;

    @Before
    public void setUp() throws Exception {
        classUnderTest = new ClassUnderTest();
    }

    @Test
    public void testAppleisSweetAndRed() throws Exception {

        exceptions.expect(Exception.class);
        exceptions.expectMessage("this is the exception message");
        exceptions.expectCause(Matchers.<Throwable>equalTo(exceptionCause));

        classUnderTest.methodUnderTest("param1", "param2");
    }

}
JoBⅈN
la source
6

Nous pouvons utiliser un échec d'assertion après la méthode qui doit retourner une exception:

try{
   methodThatThrowMyException();
   Assert.fail("MyException is not thrown !");
} catch (final Exception exception) {
   // Verify if the thrown exception is instance of MyException, otherwise throws an assert failure
   assertTrue(exception instanceof MyException, "An exception other than MyException is thrown !");
   // In case of verifying the error message
   MyException myException = (MyException) exception;
   assertEquals("EXPECTED ERROR MESSAGE", myException.getMessage());
}
Shessuky
la source
3
Le second catchavalera la trace de la pile si une autre exception est levée, perdant des informations utiles
NamshubWriter
5

En plus de ce que NamShubWriter a dit, assurez-vous que:

  • L'instance ExpectedException est publique ( question connexe )
  • L' exception ExpectedException n'est pas instanciée, par exemple, la méthode @Before. Ce message explique clairement toutes les subtilités de l'ordre d'exécution de JUnit.

Ne faites pas ceci:

@Rule    
public ExpectedException expectedException;

@Before
public void setup()
{
    expectedException = ExpectedException.none();
}

Enfin, ce billet de blog illustre clairement comment affirmer qu'une certaine exception est levée.

Bruce Wayne
la source
4

Je recommande la bibliothèque assertj-corepour gérer l'exception dans le test junit

En java 8, comme ceci:

//given

//when
Throwable throwable = catchThrowable(() -> anyService.anyMethod(object));

//then
AnyException anyException = (AnyException) throwable;
assertThat(anyException.getMessage()).isEqualTo("........");
assertThat(exception.getCode()).isEqualTo(".......);
Piotr R
la source
2

La solution Junit4 avec Java8 consiste à utiliser cette fonction:

public Throwable assertThrows(Class<? extends Throwable> expectedException, java.util.concurrent.Callable<?> funky) {
    try {
        funky.call();
    } catch (Throwable e) {
        if (expectedException.isInstance(e)) {
            return e;
        }
        throw new AssertionError(
                String.format("Expected [%s] to be thrown, but was [%s]", expectedException, e));
    }
    throw new AssertionError(
            String.format("Expected [%s] to be thrown, but nothing was thrown.", expectedException));
}

L'utilisation est alors:

    assertThrows(ValidationException.class,
            () -> finalObject.checkSomething(null));

Notez que la seule limitation est d'utiliser une finalréférence d'objet dans l'expression lambda. Cette solution permet de poursuivre les assertions de test au lieu de s'attendre à ce qu'elles puissent être testées au niveau de la méthode en utilisant la @Test(expected = IndexOutOfBoundsException.class)solution.

Donatello
la source
1

Prenez par exemple, vous voulez écrire Junit pour le fragment de code mentionné ci-dessous

public int divideByZeroDemo(int a,int b){

    return a/b;
}

public void exceptionWithMessage(String [] arr){

    throw new ArrayIndexOutOfBoundsException("Array is out of bound");
}

Le code ci-dessus consiste à tester une exception inconnue qui peut se produire et celui ci-dessous consiste à affirmer une exception avec un message personnalisé.

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

private Demo demo;
@Before
public void setup(){

    demo=new Demo();
}
@Test(expected=ArithmeticException.class)
public void testIfItThrowsAnyException() {

    demo.divideByZeroDemo(5, 0);

}

@Test
public void testExceptionWithMessage(){


    exception.expectMessage("Array is out of bound");
    exception.expect(ArrayIndexOutOfBoundsException.class);
    demo.exceptionWithMessage(new String[]{"This","is","a","demo"});
}
Shirsh Sinha
la source
1
    @Test(expectedException=IndexOutOfBoundsException.class) 
    public void  testFooThrowsIndexOutOfBoundsException() throws Exception {
         doThrow(IndexOutOfBoundsException.class).when(foo).doStuff();  
         try {
             foo.doStuff(); 
            } catch (IndexOutOfBoundsException e) {
                       assertEquals(IndexOutOfBoundsException .class, ex.getCause().getClass());
                      throw e;

               }

    }

Voici une autre façon de vérifier la méthode levée ou non.

MangduYogii
la source
1

Le framework JUnit a une assertThrows()méthode:

ArithmeticException exception = assertThrows(ArithmeticException.class, () ->
    calculator.divide(1, 0));
assertEquals("/ by zero", exception.getMessage());
Lu55
la source
0

Avec Java 8, vous pouvez créer une méthode prenant un code à vérifier et l'exception attendue comme paramètres:

private void expectException(Runnable r, Class<?> clazz) { 
    try {
      r.run();
      fail("Expected: " + clazz.getSimpleName() + " but not thrown");
    } catch (Exception e) {
      if (!clazz.isInstance(e)) fail("Expected: " + clazz.getSimpleName() + " but " + e.getClass().getSimpleName() + " found", e);
    }
  }

puis à l'intérieur de votre test:

expectException(() -> list.sublist(0, 2).get(2), IndexOutOfBoundsException.class);

Avantages:

  • ne compte sur aucune bibliothèque
  • vérification localisée - plus précise et permet d'avoir plusieurs assertions comme celle-ci dans un test si nécessaire
  • facile à utiliser
fahrenx
la source