Les exceptions dans les tests unitaires

Il existe différentes solutions pour écrire des assertions sur les exceptions. Le choix de l’une ou l’autre se fait généralement selon les tests que l’on souhaite écrire… tour d’horizon des différents choix possibles.

@Test(expected) avec JUnit

@Test(expected = IndexOutOfBoundsException.class)
public void should_throw_exception() throws Exception {
    asList(0, 1).get(2);
}

Si le test échoue le message d’erreur est :

java.lang.AssertionError: Expected exception: java.lang.IndexOutOfBoundsException

C’est la méthode la plus simple, le code est compact et le but du test est clair. On ne peut pas effectuer d’autres assertions au sein du test (celui-ci s’arrête dès que l’exception attendue est levée), c’est un bien mais aussi un mal… Le message de l’exception n’est pas testable, ce qui est pourtant bien pratique.

@Rule ExpectedException avec JUnit

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

@Test
public void should_throw_exception() throws Exception {
    thrownException.expect(IndexOutOfBoundsException.class);
    thrownException.expectMessage(containsString("2"));
    asList(0, 1).get(2);
}

Si le test échoue le message d’erreur est :

java.lang.AssertionError: Expected test to throw an instance of java.lang.IndexOutOfBoundsException

JUnit possède de nombreuses règles (@Rule) utilisables dans les tests. Celle-ci est dédiée aux exceptions. Cette solution permet, contrairement à la première, de réaliser des assertions plus précises sur l’exception levée. On peut vérifier le message, la présence ou non de cause, etc. En utilisant des matchers Hamcrest le message est testable de manière fine.

Je trouve dommage de devoir écrire ses assertions avant l’appel au code testé, l’ordre de lecture se retrouve inversé mais on s’en accommode assez bien.

@Test(expectedExceptions) avec TestNG

@Test(expectedExceptions = IndexOutOfBoundsException.class, expectedExceptionsMessageRegExp = "2")
public void should_throw_exception() throws Exception {
    asList(0, 1).get(2);
}

Si le test échoue le message d’erreur est :

Method TestNGExpectedTest.should_fail_because_no_exception_is_thrown()[...] should have thrown an exception of class java.lang.IndexOutOfBoundsException

Quand le message ne correspond pas :

org.testng.TestException: The exception was thrown with the wrong message: expected "2" but got "3"

Cette méthode avec TestNG ressemble beaucoup à celle de JUnit sauf que l’on peut tester le message ! Pratique et suffisant dans la majorité des cas. On utilise une expression régulière pour valider le message.

Etrangement expectedExceptions permet de renseigner un tableau d’Exception attendues, pas sûr que celà serve la lecture et la robustesse du test…

try / fail / catch

@Test
public void should_trow_exception() throws Exception {
    try {
        asList(0, 1).get(2);
        failBecauseExceptionWasNotThrown(IndexOutOfBoundsException.class);
    } catch (IndexOutOfBoundsException e) {
        assertThat(e).hasNoCause()
                     .hasMessage("2");
    }
}

Si le test échoue le message d’erreur est :

java.lang.AssertionError: Expected IndexOutOfBoundsException to be thrown

Quand les autres solutions ne suffisent pas, par exemple pour tester un type personnalisé avec des données spécifiques ou pour vérifier la cause d’une exception, on peut toujours se reposer sur cette solution. Le test produit est assez verbeux mais le plus gros problème à mon sens est l’oubli du fail(). Si les tests sont écrits après le code de production, il est malheureusement assez facile de l’oublier au sein du bloc try.

catch-exception

@Test
public void should_throw_exception() throws Exception {

    catchException(asList(0, 1)).get(2);

    assertThat(caughtException())
            .isInstanceOf(IndexOutOfBoundsException.class)
            .hasMessage("2")
            .hasNoCause();
}

Si le test échoue le message d’erreur est :

java.lang.AssertionError: Expecting actual not to be null

catch-exception permet également de tester l’exception levée de manière très précise mais possède l’avantage de ne pas devoir écrire ses assertions dans le bloc catch. Plus de soucis d’oubli du fail() (caughtException() retourne null si aucune exception n’est levée). Le code est également moins verbeux que le try / catch / fail.

L’outil possède d’autres variantes d’écriture documentées sur son site. La forme présentée ici est celle que je préfère, puisque malheureusement la version BDD repose sur l’utilisation de FEST-assert 1.4.

Conclusion

Avec TestNG, j’utilise expectedExceptions quand le type de l’exception et son message sont suffisants pour mon test. Sinon avec JUnit ou pour des tests plus précis, j’utilise catch-exception avec AssertJ.

comments powered by Disqus