Como afirmo minha mensagem de exceção com a anotação JUnit Test?

315

Eu escrevi alguns testes JUnit com @Testanotação. Se meu método de teste gerar uma exceção marcada e se eu desejar afirmar a mensagem junto com a exceção, existe uma maneira de fazer isso com a @Testanotação JUnit ? AFAIK, JUnit 4.7 não fornece esse recurso, mas alguma versão futura o fornece? Eu sei que no .NET você pode afirmar a mensagem e a classe de exceção. Procurando por um recurso semelhante no mundo Java.

É isso que eu quero:

@Test (expected = RuntimeException.class, message = "Employee ID is null")
public void shouldThrowRuntimeExceptionWhenEmployeeIDisNull() {}
Cshah
fonte
1
Agora que penso um pouco mais ... Tem certeza de que é uma boa ideia afirmar a mensagem? Sua pergunta me fez procurar um pouco o código-fonte junit e parece que eles poderiam ter adicionado facilmente esse recurso. O fato de que eles fizeram não , me faz pensar que não pode ser considerado uma boa prática. Por que é importante no seu projeto afirmar a mensagem?
c_maker
10
boa pergunta. Diga que um método que contém 15 linhas de código gera a mesma exceção de 2 lugares diferentes. Meus casos de teste precisam afirmar não apenas a classe de exceção, mas também a mensagem nela. Em um mundo ideal, qualquer comportamento anormal deve ter sua própria exceção. Se esse fosse o caso, minha pergunta nunca surgiria, mas os aplicativos de produção não têm sua exceção personalizada exclusiva para cada comportamento anormal.
Cshah
Como uma observação lateral - há @expectedExceptionMessageanotações no PHPUnit.
bancer 02/02

Respostas:

535

Você pode usar a @Ruleanotação com ExpectedException, assim:

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

Observe que o exemplo nos ExpectedExceptiondocumentos está (atualmente) errado - não há construtor público, então você deve usar ExpectedException.none().

Jesse Merriman
fonte
1
Nota: Para mim, quando o expectMessagefoi especificado como uma cadeia vazia, a comparação para a mensagem não foi realizada
RedDevil
1
Útil para mim. Obrigado. O método de teste deve ter throws RuntimeExceptiondepois de adicionar o código que gera uma exceção. Não pegue ...
Bumbolt
5
Pessoalmente, não gostaria de usar isso, pois criar campos com o objetivo de um pequeno subconjunto de métodos é uma prática ruim. Não é uma crítica à resposta, mas ao design da JUnit. A solução hipotética do OP seria muito melhor se existisse.
Sridhar Sarnobat
2
@redDevil: O expectMessage verifica se a mensagem de erro "contém" a cadeia especificada nesta função (como uma subcadeia de caracteres da mensagem de erro)
tuan.dinh
3
expectMessage com parâmetro de cadeia faz um verificação String.Contains, por correspondência exacta de correspondência exceção uso mensagem hamcrestfailure.expectMessage(CoreMatchers.equalTo(...))
sivabalan
42

Eu gosto da @Ruleresposta. No entanto, se por algum motivo você não quiser usar regras. Existe uma terceira opção.

@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
fonte
32

Você tem que usar @Test(expected=SomeException.class)? Quando temos que afirmar a mensagem real da exceção, é isso que fazemos.

@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
fonte
6
Estou ciente de escrever um bloco catch e usar assert dentro dele, mas para melhor legibilidade do código eu quero fazer com anotações.
Cshah
Além disso, você não receberá uma mensagem tão agradável quanto ao fazê-lo da maneira "certa".
NeplatnyUdaj
15
O problema com a versão try / catch, agora que o JUnit fornece @Test(expected=...)e ExpectedException, é que eu já vi várias vezes alguém esquecer de fazer a chamada fail()no final do trybloco . Se não for detectado pela revisão do código, seu teste poderá ser falso positivo e sempre será aprovado.
William Price
É por isso que não gosto de todas essas coisas declarativas. Isso dificulta o acesso ao que você deseja.
Sridhar Sarnobat
30

No JUnit 4.13, você pode fazer:

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

Isso também funciona no JUnit 5, mas com diferentes importações:

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

...
Johan Boberg
fonte
Como esta solução. Deve passar para JUnit 5.
WesternGun 8/19/19
Gaaaaaaaaa. 4.13 ainda está em beta a partir de hoje (outono de 2019)? mvnrepository.com/artifact/junit/junit
granadaCoder
v4.13 não está mais no estado beta (lançado em janeiro de 2020)
Simon
11

Na verdade, o melhor uso é com try / catch. Por quê? Porque você pode controlar o local onde espera a exceção.

Considere este exemplo:

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

E se um dia o código for modificado e a preparação do teste gerar uma RuntimeException? Nesse caso, o teste real nem sequer é testado e, mesmo que não gere nenhuma exceção, o teste será aprovado.

É por isso que é muito melhor usar try / catch do que confiar na anotação.

Krzysztof Cislo
fonte
Infelizmente, esta é a minha resposta também.
Sridhar Sarnobat
2
As preocupações com as alterações de código são atenuadas por ter pequenos casos de teste específicos para permutação. Às vezes, é inevitável e precisamos confiar no método catch / try, mas se isso acontecer com freqüência, é provável que tenhamos que revisar a maneira como escrevemos nossas funções de caso de teste.
Luis.espinal 04/04/19
Esse é um problema com seu teste e / ou código. Você NÃO espera uma RuntimeException geral, espera uma exceção específica ou, no mínimo, uma mensagem específica.
DennisK
Eu usei RuntimeExceptioncomo exemplo, substitua esta exceção por qualquer outra exceção.
Krzysztof Cislo
8

Raystorm teve uma boa resposta. Também não sou muito fã de regras. Faço algo semelhante, exceto que eu crio a seguinte classe de utilitário para ajudar a legibilidade e usabilidade, que é uma das grandes vantagens das anotações em primeiro lugar.

Adicione esta classe de utilitário:

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

}

Então, para o meu teste de unidade, tudo o que preciso é deste 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"
}
user64141
fonte
2

Se você estiver usando o @Rule, o conjunto de exceções será aplicado a todos os métodos de teste na classe Test.

Jyothi
fonte
2
Usando a resposta de Jesse Merriman, a exceção é verificada apenas nos métodos de teste que chamam expectExExex.expect () e ExexexexexexexMessage (). Os outros métodos usarão a definição expectEx = ExpectedException.none (), ou seja, nenhuma exceção esperada.
Egl
2

Eu nunca gostei da maneira de afirmar exceções com Junit. Se eu usar o "esperado" na anotação, parece do meu ponto de vista que estamos violando o padrão "dado, quando, então" porque o "então" é colocado na parte superior da definição de teste.

Além disso, se usarmos "@Rule", teremos que lidar com muito código de clichê. Portanto, se você pode instalar novas bibliotecas para seus testes, sugiro dar uma olhada no AssertJ (essa biblioteca agora vem com o SpringBoot)

Em seguida, um teste que não está violando os princípios "dado / quando / então" e é feito usando o AssertJ para verificar:

1 - A exceção é o que estamos esperando. 2 - Tem também uma mensagem esperada

Ficará assim:

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

Eu gosto da resposta do user64141, mas achei que poderia ser mais generalizado. Aqui está a minha opinião:

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

}

Observe que deixar a instrução "fail" dentro do bloco try faz com que a exceção de asserção relacionada seja capturada; o uso de return dentro da instrução catch evita isso.

Addison Crump
fonte
0

Importe a biblioteca de exceção-captura e use-a. É muito mais limpo que a ExpectedExceptionregra ou a try-catch.

Exemplo dos 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()
  )
);
whistling_marmot
fonte
-2
@Test (expectedExceptions = ValidationException.class, expectedExceptionsMessageRegExp = "This is not allowed")
public void testInvalidValidation() throws Exception{
     //test code
}
aasha
fonte
Alguém pode me ajudar a entender por que essa resposta é -1
aasha
A pergunta está pedindo, Junitmas a resposta está dandoTestNG
Huazhe Yin