Comment puis-je affirmer mon message d'exception avec l'annotation JUnit Test?

315

J'ai écrit quelques tests JUnit avec @Testannotation. Si ma méthode de test lève une exception vérifiée et si je veux affirmer le message avec l'exception, existe-t-il un moyen de le faire avec l' @Testannotation JUnit ? AFAIK, JUnit 4.7 ne fournit pas cette fonctionnalité mais est-ce que les futures versions la fourniront? Je sais qu'en .NET, vous pouvez affirmer le message et la classe d'exception. Vous recherchez une fonctionnalité similaire dans le monde Java.

C'est ce que je veux:

@Test (expected = RuntimeException.class, message = "Employee ID is null")
public void shouldThrowRuntimeExceptionWhenEmployeeIDisNull() {}
Cshah
la source
1
Maintenant que j'y pense un peu plus ... Êtes-vous sûr que c'est une bonne idée d'affirmer le message? Votre question m'a fait fouiller un peu dans le code source de junit et il semble qu'ils auraient pu facilement ajouter cette fonctionnalité. Le fait qu'ils ne l'ont pas fait, me fait penser que cela pourrait ne pas être considéré comme une bonne pratique. Pourquoi est-il important dans votre projet d'affirmer le message?
c_maker
10
bonne question. Dites qu'une méthode contenant 15 lignes de code lève la même exception à partir de 2 endroits différents. Mes cas de test doivent affirmer non seulement la classe d'exception mais aussi le message qu'elle contient. Dans un monde idéal, tout comportement anormal devrait avoir sa propre exception.Si tel avait été le cas, ma question ne se poserait jamais, mais les applications de production n'ont pas d'exception personnalisée unique pour chaque comportement anormal.
Cshah
En remarque - il y a une @expectedExceptionMessageannotation dans PHPUnit.
danseur le

Réponses:

535

Vous pouvez utiliser l' @Ruleannotation avec ExpectedException, comme ceci:

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

@Test
public void shouldThrowRuntimeExceptionWhenEmployeeIDisNull() throws Exception {
    expectedEx.expect(RuntimeException.class);
    expectedEx.expectMessage("Employee ID is null");

    // do something that should throw the exception...
    System.out.println("=======Starting Exception process=======");
    throw new NullPointerException("Employee ID is null");
}

Notez que l'exemple dans la ExpectedExceptiondocumentation est (actuellement) erroné - il n'y a pas de constructeur public, vous devez donc l'utiliser ExpectedException.none().

Jesse Merriman
la source
1
Remarque: Pour moi, lorsque le a expectMessageété spécifié en tant que chaîne vide, la comparaison du message n'a pas été effectuée
redDevil
1
Utile pour moi. Merci. La méthode de test devrait avoir throws RuntimeExceptionaprès avoir ajouté du code qui lève une exception. Ne l'attrapez pas ...
Bumbolt
5
Personnellement, je ne voudrais pas l'utiliser car la création de champs dans le but d'un petit sous-ensemble de méthodes est une mauvaise pratique. Pas une critique de la réponse, mais de la conception de JUnit. La solution hypothétique du PO serait tellement meilleure si elle existait.
Sridhar Sarnobat
2
@redDevil: Le message attendu vérifie si le message d'erreur "contient" la chaîne spécifiée dans cette fonction (comme une sous-chaîne du message d'erreur)
tuan.dinh
3
expectMessage avec le paramètre string effectue une vérification String.contains, pour une correspondance exacte du message d'exception, utilisez hamcrest matcherfailure.expectMessage(CoreMatchers.equalTo(...))
Sivabalan
42

J'aime la @Ruleréponse. Cependant, si pour une raison quelconque vous ne souhaitez pas utiliser de règles. Il existe une troisième option.

@Test (expected = RuntimeException.class)
public void myTestMethod()
{
   try
   {
      //Run exception throwing operation here
   }
   catch(RuntimeException re)
   {
      String message = "Employee ID is null";
      assertEquals(message, re.getMessage());
      throw re;
    }
    fail("Employee Id Null exception did not throw!");
  }
Raystorm
la source
32

Devez-vous utiliser @Test(expected=SomeException.class)? Lorsque nous devons affirmer le message réel de l'exception, c'est ce que nous faisons.

@Test
public void myTestMethod()
{
  try
  {
    final Integer employeeId = null;
    new Employee(employeeId);
    fail("Should have thrown SomeException but did not!");
  }
  catch( final SomeException e )
  {
    final String msg = "Employee ID is null";
    assertEquals(msg, e.getMessage());
  }
}
c_maker
la source
6
Je suis conscient d'écrire un bloc catch et d'utiliser assert dans cela, mais pour une meilleure lisibilité du code, je veux faire des annotations.
Cshah
De plus, vous n'obtiendrez pas un message aussi agréable que lorsque vous le faites de la "bonne" manière.
NeplatnyUdaj
15
Le problème avec la version try / catch, maintenant que JUnit fournit @Test(expected=...)et ExpectedException, c'est que j'ai vu à plusieurs reprises quelqu'un oublier de mettre l'appel fail()à la fin du trybloc . S'il n'est pas détecté par la révision du code, votre test peut être faux positif et toujours réussir.
William Price du
C'est pourquoi je n'aime pas toutes ces choses déclaratives. Cela rend difficile l'accès à ce que vous voulez.
Sridhar Sarnobat
30

Dans JUnit 4.13, vous pouvez faire:

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;

...

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

  assertEquals("a message", exception.getMessage());
}

Cela fonctionne également dans JUnit 5 mais avec des importations différentes:

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

...
Johan Boberg
la source
Comme cette solution. Devrait passer à JUnit 5.
WesternGun
Gaaaaaaaaa. 4.13 toujours en version bêta à ce jour (automne 2019)? mvnrepository.com/artifact/junit/junit
granadaCoder
la v4.13 n'est plus en version beta (sortie en janvier 2020)
Simon
11

En fait, la meilleure utilisation est avec try / catch. Pourquoi? Parce que vous pouvez contrôler l'endroit où vous attendez l'exception.

Considérez cet exemple:

@Test (expected = RuntimeException.class)
public void someTest() {
   // test preparation
   // actual test
}

Que se passe-t-il si un jour le code est modifié et que la préparation du test génère une exception RuntimeException? Dans ce cas, le test réel n'est même pas testé et même s'il ne déclenche aucune exception, le test réussit.

C'est pourquoi il est préférable d'utiliser try / catch que de s'appuyer sur l'annotation.

Krzysztof Cislo
la source
Malheureusement, c'est aussi ma réponse.
Sridhar Sarnobat
2
Les préoccupations concernant les changements de code sont atténuées par la présence de petits cas de test spécifiques à la permutation. Parfois, c'est inévitable et nous devons nous fier à la méthode catch / try, mais si cela se produit fréquemment, il est probable que nous devions réviser la façon dont nous écrivons nos fonctions de cas de test.
luis.espinal
C'est un problème avec votre test et / ou votre code. Vous ne vous attendez PAS à une exception RuntimeException générale, vous vous attendez à une exception spécifique ou, à tout le moins, à un message spécifique.
DennisK
J'ai utilisé RuntimeExceptioncomme exemple, remplacez cette exception par toute autre exception.
Krzysztof Cislo
8

Raystorm avait une bonne réponse. Je ne suis pas non plus un grand fan des règles. Je fais quelque chose de similaire, sauf que je crée la classe d'utilité suivante pour aider la lisibilité et l'utilisabilité, qui est l'un des gros avantages des annotations en premier lieu.

Ajoutez cette classe d'utilitaires:

import org.junit.Assert;

public abstract class ExpectedRuntimeExceptionAsserter {

    private String expectedExceptionMessage;

    public ExpectedRuntimeExceptionAsserter(String expectedExceptionMessage) {
        this.expectedExceptionMessage = expectedExceptionMessage;
    }

    public final void run(){
        try{
            expectException();
            Assert.fail(String.format("Expected a RuntimeException '%s'", expectedExceptionMessage));
        } catch (RuntimeException e){
            Assert.assertEquals("RuntimeException caught, but unexpected message", expectedExceptionMessage, e.getMessage());
        }
    }

    protected abstract void expectException();

}

Ensuite, pour mon test unitaire, tout ce dont j'ai besoin est ce code:

@Test
public void verifyAnonymousUserCantAccessPrivilegedResourceTest(){
    new ExpectedRuntimeExceptionAsserter("anonymous user can't access privileged resource"){
        @Override
        protected void expectException() {
            throw new RuntimeException("anonymous user can't access privileged resource");
        }
    }.run(); //passes test; expected exception is caught, and this @Test returns normally as "Passed"
}
user64141
la source
2

Si vous utilisez @Rule, le jeu d'exceptions est appliqué à toutes les méthodes de test de la classe Test.

Jyothi
la source
2
À l'aide de la réponse de Jesse Merriman, l'exception est uniquement vérifiée dans les méthodes de test qui appellent vers attenduEx.expect () et attenduEx.expectMessage (). Les autres méthodes utiliseront la définition attenduEx = ExpectedException.none (), c'est-à-dire qu'aucune exception n'est attendue.
Egl
2

Je n'ai jamais aimé la façon d'affirmer des exceptions avec Junit. Si j'utilise le «attendu» dans l'annotation, il me semble que nous violons le modèle «donné, quand, puis» parce que le «alors» est placé en haut de la définition du test.

De plus, si nous utilisons "@Rule", nous devons faire face à tant de code standard. Donc, si vous pouvez installer de nouvelles bibliothèques pour vos tests, je vous suggère de jeter un œil à AssertJ (cette bibliothèque est désormais livrée avec SpringBoot)

Ensuite, un test qui ne viole pas les principes "donnés / quand / alors", et il est fait en utilisant AssertJ pour vérifier:

1 - L'exception est ce que nous attendons. 2 - Il a aussi un message attendu

Ressemblera à ceci:

 @Test
void should_throwIllegalUse_when_idNotGiven() {

    //when
    final Throwable raisedException = catchThrowable(() -> getUserDAO.byId(null));

    //then
    assertThat(raisedException).isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("Id to fetch is mandatory");
}
geeksusma
la source
1

J'aime la réponse de user64141 mais j'ai trouvé qu'elle pourrait être plus généralisée. Voici mon point de vue:

public abstract class ExpectedThrowableAsserter implements Runnable {

    private final Class<? extends Throwable> throwableClass;
    private final String expectedExceptionMessage;

    protected ExpectedThrowableAsserter(Class<? extends Throwable> throwableClass, String expectedExceptionMessage) {
        this.throwableClass = throwableClass;
        this.expectedExceptionMessage = expectedExceptionMessage;
    }

    public final void run() {
        try {
            expectException();
        } catch (Throwable e) {
            assertTrue(String.format("Caught unexpected %s", e.getClass().getSimpleName()), throwableClass.isInstance(e));
            assertEquals(String.format("%s caught, but unexpected message", throwableClass.getSimpleName()), expectedExceptionMessage, e.getMessage());
            return;
        }
        fail(String.format("Expected %s, but no exception was thrown.", throwableClass.getSimpleName()));
    }

    protected abstract void expectException();

}

Notez que laisser l'instruction "fail" dans le bloc try provoque la capture de l'exception d'assertion associée; l'utilisation de return dans l'instruction catch empêche cela.

Addison Crump
la source
0

Importez la bibliothèque catch-exception et utilisez-la. C'est beaucoup plus propre que la ExpectedExceptionrègle ou a try-catch.

Exemple de leurs documents:

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

// given: an empty list
List myList = new ArrayList();

// when: we try to get the first element of the list
catchException(myList).get(1);

// then: we expect an IndexOutOfBoundsException with message "Index: 1, Size: 0"
assertThat(caughtException(),
  allOf(
    instanceOf(IndexOutOfBoundsException.class),
    hasMessage("Index: 1, Size: 0"),
    hasNoCause()
  )
);
whistling_marmot
la source
-2
@Test (expectedExceptions = ValidationException.class, expectedExceptionsMessageRegExp = "This is not allowed")
public void testInvalidValidation() throws Exception{
     //test code
}
aasha
la source
Quelqu'un peut-il m'aider à comprendre pourquoi cette réponse est un -1
aasha
La question demande Junitmais votre réponse donneTestNG
Huazhe Yin