web-dev-qa-db-fra.com

En Java, comment valider une exception levée avec JUnit?

Lors de l'écriture de tests unitaires pour une API Java, il peut arriver que vous souhaitiez effectuer une validation plus détaillée d'une exception. C'est à dire. plus que ce qui est offert par l'annotation @test offerte par JUnit.

Par exemple, considérons une classe qui devrait intercepter une exception d'une autre interface, encapsule cette exception et lève l'exception encapsulée. Vous voudrez peut-être vérifier:

  1. L'appel de méthode exact qui lève l'exception encapsulée.
  2. Que l'exception wrapper ait l'exception d'origine comme cause.
  3. Le message de l'exception wrapper.

Le point principal ici est que vous souhaitez effectuer une validation supplémentaire d’une exception dans un test unitaire (et non un débat sur le fait de savoir si devrait vérifier des éléments comme le message d’exception).

Quelle est la bonne approche pour cela?

28
Iain

Dans JUnit 4, cela peut être facilement fait avec ExpectedException rule.

Voici un exemple de javadocs:

// These tests all pass.
public static class HasExpectedException {
    @Rule
    public ExpectedException thrown = ExpectedException.none();

    @Test
    public void throwsNothing() {
        // no exception expected, none thrown: passes.
    }

    @Test
    public void throwsNullPointerException() {
        thrown.expect(NullPointerException.class);
        throw new NullPointerException();
    }

    @Test
    public void throwsNullPointerExceptionWithMessage() {
        thrown.expect(NullPointerException.class);
        thrown.expectMessage("happened?");
        thrown.expectMessage(startsWith("What"));
        throw new NullPointerException("What happened?");
    }
}
21
Jonas

Comme indiqué dans votre réponse , c'est une bonne approche. De plus:

Vous pouvez envelopper la fonction expectException dans une nouvelle annotation, appelée ExpectedException.
Une méthode annotée ressemblerait à ceci:

@Test
@ExpectedException(class=WrapperException.class, message="Exception Message", causeException)
public void testAnExceptionWrappingFunction() {
  //whatever you test
}

De cette façon, ce serait plus lisible, mais c'est exactement la même approche.

Une autre raison est: J'aime les annotations :)

24
guerda

En regardant les réponses proposées, vous pouvez vraiment ressentir la douleur de ne pas avoir de fermeture à Java. À mon humble avis, la solution la plus lisible est la bonne vieille tentative de capture.

@Test
public void test() {
    ...
    ...
    try {
        ...
        fail("No exception caught :(");
    }
    catch (RuntimeException ex) {
        assertEquals(Whatever.class, ex.getCause().getClass());
        assertEquals("Message", ex.getMessage());
    }
}
18
akuhn

Pour JUNIT 3.x

public void test(){
   boolean thrown = false;
   try{
      mightThrowEx();
   } catch ( Surprise expected ){
      thrown = true;
      assertEquals( "message", expected.getMessage());
   }
  assertTrue(thrown );
}
11
mP.

Jusqu'à ce poste, j'ai validé mon exception en faisant ceci:

try {
    myObject.doThings();
    fail("Should've thrown SomeException!");
} catch (SomeException e) {
    assertEquals("something", e.getSomething());
}

J’ai passé quelques instants à réfléchir au problème et j’ai proposé ce qui suit (Java5, JUnit 3.x):

// Functor interface for exception assertion.
public interface AssertionContainer<T extends Throwable> {
    void invoke() throws T;
    void validate(T throwable);
    Class<T> getType();
}

// Actual assertion method.
public <T extends Throwable> void assertThrowsException(AssertionContainer<T> functor) {
    try {
        functor.invoke();
        fail("Should've thrown "+functor.getType()+"!");
    } catch (Throwable exc) {
        assertSame("Thrown exception was of the wrong type! Expected "+functor.getClass()+", actual "+exc.getType(),
                   exc.getClass(), functor.getType());
        functor.validate((T) exc);
    }
}

// Example implementation for servlet I used to actually test this. It was an inner class, actually.
AssertionContainer<ServletException> functor = new AssertionContainer<ServletException>() {
    public void invoke() throws ServletException {
        servlet.getRequiredParameter(request, "some_param");
    }

    public void validate(ServletException e) {
        assertEquals("Parameter \"some_param\" wasn't found!", e.getMessage());
    }

    public Class<ServletException> getType() {
        return ServletException.class;
    }
}

// And this is how it's used.
assertThrowsException(functor);

En regardant ces deux-là, je ne peux pas décider lequel me plait le plus. J'imagine que c'est l'une de ces questions où atteindre un objectif (dans mon cas, la méthode d'assertion avec le paramètre foncteur) n'en vaut pas la peine à long terme, car il est beaucoup plus facile de faire ces 6+ de code pour affirmer l'essai bloc ..catch.

Encore une fois, peut-être que mon résultat de 10 minutes de résolution de problèmes de vendredi soir n’est tout simplement pas la façon la plus intelligente de le faire.

5
Esko

@akuhn:

Même sans fermeture, nous pouvons obtenir une solution plus lisible (en utilisant catch-exception ):

import static com.googlecode.catchexception.CatchException.*;

public void test() {
    ...
    ...
    catchException(nastyBoy).doNastyStuff();
    assertTrue(caughtException() instanceof WhateverException);
    assertEquals("Message", caughtException().getMessage());
}
4
rwitzel

j'ai fait quelque chose de très simple

testBla(){
    try {
        someFailingMethod()
        fail(); //method provided by junit
    } catch(Exception e) {
          //do nothing
    }
}
2
Andreas Petersson

La méthode d'assistance suivante (adaptée de this blog post) fait l'affaire:

/**
 * Run a test body expecting an exception of the
 * given class and with the given message.
 *
 * @param test              To be executed and is expected to throw the exception.
 * @param expectedException The type of the expected exception.
 * @param expectedMessage   If not null, should be the message of the expected exception.
 * @param expectedCause     If not null, should be the same as the cause of the received exception.
 */
public static void expectException(
        Runnable test,
        Class<? extends Throwable> expectedException,
        String expectedMessage,
        Throwable expectedCause) {
    try {
        test.run();
    }
    catch (Exception ex) {
        assertSame(expectedException, ex.getClass());
        if (expectedMessage != null) {
            assertEquals(expectedMessage, ex.getMessage());
        }

        if (expectedCause != null) {
            assertSame(expectedCause, ex.getCause());
        }

        return;
    }

    fail("Didn't find expected exception of type " + expectedException.getName());
}

Le code de test peut alors invoquer ceci comme suit:

TestHelper.expectException(
        new Runnable() {
            public void run() {
                classInstanceBeingTested.methodThatThrows();
            }
        },
        WrapperException.class,
        "Exception Message",
        causeException
);
2
Iain

Pour JUnit 5 c'est beaucoup plus facile:

    @Test
    void testAppleIsSweetAndRed() throws Exception {

        IllegalArgumentException ex = assertThrows(
                IllegalArgumentException.class,
                () -> testClass.appleIsSweetAndRed("orange", "red", "sweet"));

        assertEquals("this is the exception message", ex.getMessage());
        assertEquals(NullPointerException.class, ex.getCause().getClass());
    }

En renvoyant l'objet exception lui-même, assertThrows() vous permet de tester chaque aspect des exceptions levées.

0
Marcus K.

J'ai fait une aide semblable aux autres postés:

public class ExpectExceptionsExecutor {

    private ExpectExceptionsExecutor() {
    }

    public static  void execute(ExpectExceptionsTemplate e) {
        Class<? extends Throwable> aClass = e.getExpectedException();

        try {
            Method method = ExpectExceptionsTemplate.class.getMethod("doInttemplate");
            method.invoke(e);
        } catch (NoSuchMethodException e1) {


            throw new RuntimeException();
        } catch (InvocationTargetException e1) {


            Throwable throwable = e1.getTargetException();
            if (!aClass.isAssignableFrom(throwable.getClass())) {
                //  assert false
                fail("Exception isn't the one expected");
            } else {
                assertTrue("Exception captured ", true);
                return;
            }
            ;


        } catch (IllegalAccessException e1) {
            throw new RuntimeException();
        }

        fail("No exception has been thrown");
    }


}

Et le modèle que le client doit implémenter 

public interface ExpectExceptionsTemplate<T extends Throwable> {


    /**
     * Specify the type of exception that doInttemplate is expected to throw
     * @return
     */
    Class<T> getExpectedException();


    /**
     * Execute risky code inside this method
     * TODO specify expected exception using an annotation
     */
    public void doInttemplate();

}

Et le code client ressemblerait à ceci:

@Test
    public void myTest() throws Exception {
        ExpectExceptionsExecutor.execute(new ExpectExceptionsTemplate() {
            @Override
            public Class getExpectedException() {
                return IllegalArgumentException.class;
            }

            @Override
            public void doInttemplate() {
                riskyMethod.doSomething(null);
            }
        });
     }

Cela semble vraiment détaillé, mais si vous utilisez un IDE avec une bonne complétion automatique, il vous suffira d'écrire le type d'exception et le code à tester. (le reste sera fait par le IDE: D)

0
Jaime Hablutzel