web-dev-qa-db-fra.com

Comment vérifier qu'une exception n'a pas été levée

Dans mon test unitaire avec Mockito, je veux vérifier que NullPointerException n'a pas été lancé.

public void testNPENotThrown{
    Calling calling= Mock(Calling.class);
    testClass.setInner(calling);
    testClass.setThrow(true);

    testClass.testMethod();

    verify(calling, never()).method();
}

Mon test a configuré le testClass, en définissant l'objet Calling et la propriété de sorte que la méthode lance un NullPointerException.

Je vérifie que Calling.method () n'est jamais appelé.

public void testMethod(){
    if(throw) {
        throw new NullPointerException();
    }

    calling.method();
}

Je veux avoir un test qui échoue car il jette un NullPointerException, puis je veux écrire du code pour résoudre ce problème.

Ce que j'ai remarqué, c'est que le test réussit toujours car l'exception n'est jamais levée dans la méthode de test.

17
well_i

tl; dr

  • pré-JDK8: Je recommanderai l'ancien bon bloc try-catch.

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

la longue histoire

Il est possible de vous écrire un bloc faites-le vous-mêmetry-catch ou utilisez les outils JUnit (@Test(expected = ...) ou @Rule ExpectedException Fonction de règle JUnit).

Mais ces méthodes ne sont pas si élégantes et ne se mélangent pas bien en termes de lisibilité avec d'autres outils.

  1. Le bloc try-catch vous devez écrire le bloc autour du comportement testé, et écrire l'assertion dans le bloc catch, ce qui peut être correct mais beaucoup trouvent que ce style interrompt le flux de lecture de un examen. Vous devez également écrire un Assert.fail À la fin du bloc try sinon le test risque de manquer un côté des assertions; [~ # ~] pmd [~ # ~], findbugs ou Sonar détectera ces problèmes.

  2. La fonction @Test(expected = ...) 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 faux test positif et je ne suis pas sûr que [~ # ~] pmd [~ # ~], findbugs ou Sonar donnera des indices 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 règle ExpectedException est également une tentative de corriger les mises en garde précédentes, mais elle semble un peu gênante à utiliser car elle utilise un style d'attente, EasyMock les utilisateurs connaissent très bien ce style. Cela peut être pratique pour certains, mais si vous suivez Behavior Driven Development (BDD) ou Arrange Act Assert (AAA), les principes ExpectedException la règle ne rentrera pas dans ces styles d'écriture. En plus de cela, il peut souffrir du même problème que la méthode @Test, 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 la déclaration de test, elle rompt votre flux de lecture si les tests suivent BDD ou AAA.

    Voir aussi ce problème comment sur JUnit de l'auteur de ExpectedException.

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 que j'ai pris conscience après avoir créé cette réponse qui semble prometteur, c'est catch-exception .

    Comme le dit la description du projet, il a laissé un codeur écrire dans une ligne de code fluide interceptant l'exception et proposer cette exception pour une affirmation ultérieure. 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'API then est un alias qui utilisera les API AssertJ (similaire à l'utilisation de 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 Java 8 support Lambdas.

    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 créateur de maquette global (inline-mock-maker), Ce qui peut ne pas être ce que vous voulez, car ce simulateur a différents inconvénients que le simulateur ordinaire .

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

    Ces problèmes ne s'appliqueront pas une fois que la bibliothèque prendra en charge les lambdas, mais 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'ancien bon moyen du bloc try-catch , au moins jusqu'au JDK7. Et pour les utilisateurs de JDK 8, vous préférerez peut-être utiliser AssertJ car il offre plus que simplement affirmer des 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 API couramment utilisée 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é améliorées un peu, 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 en dehors 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é, assertEquals renvoie toujours void et, en tant que tel, ne permet pas de chaîner des assertions comme AssertJ.

    De plus, si vous vous souvenez d'un conflit de noms avec Matcher ou 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 de facto la dépendance de test est la meilleure solution avec JDK8 quel que soit le framework de test (JUnit ou non), les JDK antérieurs devraient plutôt s'appuyer sur try-catch bloque même s'ils semblent maladroits.

17
Brice

Si je ne vous comprends pas, vous avez besoin de quelque chose comme ceci:

@Test(expected = NullPointerException.class)
public void testNPENotThrown {
    Calling calling= Mock(Calling .class);
    testClass.setInner(calling);
    testClass.setThrow(true);

    testClass.testMethod();

    verify(calling, never()).method();
    Assert.fail("No NPE");
}

mais en nommant le test "NPENotThrown", je m'attendrais à un test comme celui-ci:

public void testNPENotThrown {
    Calling calling= Mock(Calling .class);
    testClass.setInner(calling);
    testClass.setThrow(true);

    testClass.testMethod();
    try {
        verify(calling, never()).method();
        Assert.assertTrue(Boolean.TRUE);
    } catch(NullPointerException ex) {
        Assert.fail(ex.getMessage());
    }
}
5
Stefan Beike

Une autre approche pourrait être d'utiliser à la place try/catch. C'est un peu désordonné, mais d'après ce que je comprends, ce test sera de toute façon de courte durée car c'est pour TDD:

@Test
public void testNPENotThrown{
  Calling calling= Mock(Calling.class);
  testClass.setInner(calling);
  testClass.setThrow(true);

  try{
    testClass.testMethod();
    fail("NPE not thrown");
  }catch (NullPointerException e){
    //expected behaviour
  }
}

EDIT: J'étais pressé quand j'ai écrit ça. Ce que je veux dire par `` ce test sera de toute façon de courte durée car c'est pour TDD '', c'est que vous dites que vous allez écrire du code pour corriger ce test tout de suite, donc il ne lèvera jamais d'exception NullPointerException à l'avenir. Vous pouvez alors aussi supprimer le test. Par conséquent, cela ne vaut probablement pas la peine de passer beaucoup de temps à écrire un beau test (d'où ma suggestion :-))

Plus généralement:

Commencer par un test pour affirmer que (par exemple) la valeur de retour d'une méthode n'est pas nulle est un principe TDD établi, et la recherche d'une NullPointerException (NPE) est une façon possible de procéder. Cependant, votre code de production ne va probablement pas avoir de flux où un NPE est lancé. Vous allez vérifier la nullité puis faire quelque chose de sensé, j'imagine. Cela rendrait ce test particulier redondant à ce stade car il vérifiera qu'un NPE n'est pas lancé, alors qu'en fait, cela ne peut jamais se produire. Vous pouvez ensuite le remplacer par un test qui vérifie ce qui se produit lorsqu'un null est rencontré: retourne un NullObject par exemple, ou lève un autre type d'exception, selon ce qui est approprié.

Il n'est bien sûr pas nécessaire de supprimer le test redondant, mais si vous ne le faites pas, il restera là, ce qui rendra chaque build légèrement plus lent et amènera chaque développeur qui lit le test à se demander; "Hmm, un NPE? Ce code ne peut sûrement pas lancer un NPE?". J'ai vu beaucoup de code TDD où les classes de test ont beaucoup de tests redondants comme celui-ci. Si le temps le permet, il est utile de revoir vos tests de temps en temps.

2
Mark Chorley

Généralement, chaque scénario de test s'exécute avec une nouvelle instance, donc la définition d'une variable d'instance n'aidera pas. Donc, rendez la variable 'throw' statique sinon.

0
Juned Ahsan