¿Cómo hago valer mi mensaje de excepción con la anotación JUnit Test?

315

He escrito algunas pruebas JUnit con @Testanotaciones. Si mi método de prueba arroja una excepción marcada y si quiero afirmar el mensaje junto con la excepción, ¿hay alguna manera de hacerlo con la @Testanotación JUnit ? AFAIK, JUnit 4.7 no proporciona esta función, pero ¿alguna versión futura la proporciona? Sé que en .NET puedes afirmar el mensaje y la clase de excepción. Buscando una característica similar en el mundo de Java.

Esto es lo que quiero:

@Test (expected = RuntimeException.class, message = "Employee ID is null")
public void shouldThrowRuntimeExceptionWhenEmployeeIDisNull() {}
Cshah
fuente
1
Ahora que lo pienso un poco más ... ¿Estás seguro de que es una buena idea hacer valer el mensaje? Su pregunta me hizo profundizar un poco en el código fuente junit y parece que podrían haber agregado fácilmente esta función. El hecho de que no lo hicieron me hace pensar que podría no considerarse una buena práctica. ¿Por qué es importante en su proyecto hacer valer el mensaje?
c_maker
10
buena pregunta. Diga que un método que contiene 15 líneas de código arroja la misma excepción desde 2 lugares diferentes. Mis casos de prueba necesitan afirmar no solo la clase de excepción sino también el mensaje que contiene. En un mundo ideal, cualquier comportamiento anormal debería tener su propia excepción. Si ese hubiera sido el caso, mi pregunta nunca surgiría, pero las aplicaciones de producción no tienen su única excepción personalizada para cada comportamiento anormal.
Cshah
Como nota al margen, hay una @expectedExceptionMessageanotación en PHPUnit.
bancer

Respuestas:

535

Podrías usar la @Ruleanotación con ExpectedException, así:

@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");
}

Tenga en cuenta que el ejemplo en los ExpectedExceptiondocumentos es (actualmente) incorrecto: no hay un constructor público, por lo que debe usarlo ExpectedException.none().

Jesse Merriman
fuente
1
Nota: Para mí, cuando expectMessagese especificó como una cadena vacía, no se realizó la comparación para el mensaje
redDevil
1
Útil para mi. Gracias. El método de prueba debería tener throws RuntimeExceptiondespués de agregar código que arroja una excepción. No lo atrapes ...
Bumbolt
55
Personalmente, no me gustaría usar esto ya que crear campos con el propósito de un pequeño subconjunto de métodos es una mala práctica. No es una crítica de la respuesta, sino del diseño de JUnit. La solución hipotética del OP sería mucho mejor si existiera.
Sridhar Sarnobat
2
@redDevil: El mensaje esperado comprueba si el mensaje de error "contiene" la cadena especificada en esta función (como una subcadena del mensaje de error)
tuan.dinh
3
expectMessage con el parámetro de cadena realiza una comprobación de String.contains, para la coincidencia exacta del mensaje de excepción use hamcrest matcherfailure.expectMessage(CoreMatchers.equalTo(...))
Sivabalan
42

Me gusta la @Rulerespuesta Sin embargo, si por alguna razón no quieres usar reglas. Hay una tercera opción.

@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!");
  }
Tormenta de rayos
fuente
32

¿Tienes que usar @Test(expected=SomeException.class)? Cuando tenemos que afirmar el mensaje real de la excepción, esto es lo que hacemos.

@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
fuente
66
Soy consciente de escribir un bloque catch y usar asir dentro de eso, pero para una mejor legibilidad del código, quiero hacer anotaciones.
Cshah
Además, no recibirá un mensaje tan agradable como cuando lo hace de la manera "correcta".
NeplatnyUdaj
15
El problema con la versión try / catch, ahora que JUnit proporciona @Test(expected=...)y ExpectedException, es que he visto en numerosas ocasiones a alguien olvidando poner la llamada al fail()final del trybloque . Si no lo detecta la revisión de código, su prueba puede ser falsa positiva y siempre pasar.
William Price
Es por eso que no me gustan todas estas cosas declarativas. Hace que sea difícil acceder a lo que quieres.
Sridhar Sarnobat
30

En JUnit 4.13 puedes hacer:

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());
}

Esto también funciona en JUnit 5 pero con diferentes importaciones:

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

...
Johan Boberg
fuente
Me gusta esta solución. Debería mudarse a JUnit 5.
WesternGun
Gaaaaaaaaa. 4.13 todavía en beta a partir de hoy (otoño, 2019)? mvnrepository.com/artifact/junit/junit
granadaCoder
v4.13 ya no está en estado beta (lanzamiento en enero de 2020)
Simon
11

En realidad, el mejor uso es con try / catch. ¿Por qué? Porque puedes controlar el lugar donde esperas la excepción.

Considere este ejemplo:

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

¿Qué pasa si un día se modifica el código y la preparación de la prueba arrojará una RuntimeException? En ese caso, la prueba real ni siquiera se prueba e incluso si no arroja ninguna excepción, la prueba pasará.

Es por eso que es mucho mejor usar try / catch que confiar en la anotación.

Krzysztof Cislo
fuente
Lamentablemente, esta es mi respuesta también.
Sridhar Sarnobat
2
Las preocupaciones con respecto a los cambios en el código se alivian al tener pequeños casos de prueba específicos de permutación. A veces es inevitable y tenemos que confiar en el método catch / try, pero si eso sucede con frecuencia, entonces es probable que necesitemos revisar la forma en que escribimos nuestras funciones de casos de prueba.
luis.espinal
Eso es un problema con su prueba y / o código. NO espera una RuntimeException general, espera una excepción específica, o al menos un mensaje específico.
DennisK
He utilizado RuntimeExceptioncomo un ejemplo, reemplazar esta excepción con cualquier otra excepción.
Krzysztof Cislo
8

Raystorm tuvo una buena respuesta. Tampoco soy un gran admirador de las reglas. Hago algo similar, excepto que creo la siguiente clase de utilidad para ayudar a la legibilidad y usabilidad, que es una de las grandes ventajas de las anotaciones en primer lugar.

Agregue esta clase de utilidad:

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();

}

Luego, para mi prueba unitaria, todo lo que necesito es este código:

@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"
}
usuario64141
fuente
2

Si usa @Rule, el conjunto de excepciones se aplica a todos los métodos de prueba en la clase Prueba.

Jyothi
fuente
2
Con la respuesta de Jesse Merriman, la excepción solo se verifica en los métodos de prueba que llaman a expectEx.expect () y expectEx.expectMessage (). Los otros métodos usarán la definición esperadaExExExceptionException.none (), es decir, no se espera ninguna excepción.
Egl
2

Nunca me gustó la forma de afirmar excepciones con Junit. Si uso el "esperado" en la anotación, parece que desde mi punto de vista estamos violando el patrón "dado, cuándo, entonces" porque el "entonces" se coloca en la parte superior de la definición de la prueba.

Además, si usamos "@Rule", tenemos que lidiar con tanto código repetitivo. Entonces, si puede instalar nuevas bibliotecas para sus pruebas, le sugiero que eche un vistazo a AssertJ (esa biblioteca ahora viene con SpringBoot)

Luego, una prueba que no está violando los principios "dado / cuándo / entonces", y se realiza utilizando AssertJ para verificar:

1 - La excepción es lo que esperamos. 2 - También tiene un mensaje esperado

Se verá así:

 @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
fuente
1

Me gusta la respuesta de user64141 pero descubrí que podría ser más generalizada. Aquí está mi opinión:

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();

}

Tenga en cuenta que si deja la instrucción "fail" dentro del bloque try, se detecta la excepción de aserción relacionada; El uso de return dentro de la instrucción catch evita esto.

Addison Crump
fuente
0

Importe la biblioteca de excepción de captura y úsela. Es mucho más limpio que la ExpectedExceptionregla o a try-catch.

Ejemplo de sus documentos:

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()
  )
);
silbido_marmota
fuente
-2
@Test (expectedExceptions = ValidationException.class, expectedExceptionsMessageRegExp = "This is not allowed")
public void testInvalidValidation() throws Exception{
     //test code
}
aasha
fuente
¿Puede alguien ayudarme a entender por qué esta respuesta es un -1
Aasha
La pregunta es pregunta Junitpero tu respuesta es darTestNG
Huazhe Yin