Como testar exceções em Java com o JUnit

Introdução

Neste post vou mostrar de forma sucinta e prática como testar exceções em Java, utilizando o framework JUnit 4. Vou demonstrar 3 formas de escrever testes unitários que verificam o comportamento de exceções.

Nossa classe para ser testada

As 3 formas de se testar exceções em Java serão feitas em cima da classe mostrada abaixo.

public class TransferenciaBancariaService {

	private TransferenciaBancariaRepository repository = new TransferenciaBancariaRepository();

	public void transfere(ContaBancaria origem, ContaBancaria destino, BigDecimal valor) {
		validarTransferencia(destino, valor);
		origem.debita(valor);
		destino.deposita(valor);
		repository.salvar(origem);
		repository.salvar(destino);
	}

	private void validarTransferencia(ContaBancaria destino, BigDecimal valor) {
		if (destino.getSaldo().compareTo(valor) < 0) {
			throw new SaldoInsuficienteException(destino);
		}
	}
}

O código é simples. Faz a transferência de um valor entre uma conta origem e uma conta destino. Se o saldo da conta origem não for suficiente para a realização da transferência, uma exceção será lançada.

Abaixo o código da exceção.

public class SaldoInsuficienteException extends RuntimeException {

	public static final String SALDO_INSUFICIENTE_MSG = "A conta %s não possui saldo suficiente";

	public SaldoInsuficienteException(ContaBancaria destino) {
		super(String.format(SALDO_INSUFICIENTE_MSG, destino.toString()));
		
	}
}

Vamos aos testes.

Como testar exceções em Java #1: @Test(expected=...)

O JUnit 4 permite definirmos, opcionalmente, como argumento da anotação @Test o expected.

O expected nos permite especificar uma exceção que esperamos que seja lançada pelo código sendo testado. O teste só tem sucesso se a exceção for lançada, caso contrário temos uma falha.

O exemplo abaixo mostra o código de teste com o expected.

public class TransferenciaBancariaServiceTest {

  private TransferenciaBancariaService service = new TransferenciaBancariaService();

  @Test(expected = SaldoInsuficienteException.class)
  public void transferenciaImpossivelSaldoInsuficienteDeveLancarExcecao() {
    ContaBancaria origem = new ContaBancaria("CONTA-A", BigDecimal.valueOf(1500l));
    ContaBancaria destino = new ContaBancaria("CONTA-B", BigDecimal.valueOf(500l));

    service.transfere(origem, destino, BigDecimal.valueOf(2000l));
  }

}

O código de teste acima deve lançar uma exceção, pois o valor que desejamos transferir – 2000 – é maior que o saldo da conta origem: 1500.

O argumento expected=SaldoInsuficienteException.class diz ao JUnit que esperamos que o teste ao executar lance uma exceção do tipo SaldoInsuficienteException. Veja que não precisamos fazer asserções nesse código; o argumento expected de @Test faz esse papel.

Embora o teste anterior seja legal e bastante conciso, ele não atende a um ponto: e se quisermos verificar a mensagem que foi lançada? Nesse caso, vamos ter que usar uma abordagem diferente. Vamos a ela.

Como testar exceções em Java #2: blocos try {} catch()

Uma outra forma de como testar exceções em Java é utilizando um bloco try{} catch. Essa forma era bastante utilizada no JUnit 3, antes de aparecer a funcionalidade @Test(expected=...).

Embora essa forma de se testar exceções não seja mais tão utilizada, ela ainda é útil quando queremos verificar a mensagem de detalhe da exceção disparada. Isso ocorre, por exemplo, se a mensagem da exceção vai ser exibida na camada de apresentação para o usuário. Nesse caso, é legal validarmos se a mensagem é aquela que esperamos.

Vamos ao código de teste com try{} catch().

public class TransferenciaBancariaServiceTest {

  private TransferenciaBancariaService service = new TransferenciaBancariaService();

  @Test
  public void transferenciaImpossivelSaldoInsuficienteDeveLancarExcecao() {
    ContaBancaria origem = new ContaBancaria("CONTA-A", BigDecimal.valueOf(1500l));
    ContaBancaria destino = new ContaBancaria("CONTA-B", BigDecimal.valueOf(500l));

    try {
      service.transfere(origem, destino, BigDecimal.valueOf(2000l));
      fail("Falha. Uma exceção deve ser lançada!");
    } catch (SaldoInsuficienteException ex) {
      assertEquals(String.format(SaldoInsuficienteException.SALDO_INSUFICIENTE_MSG, origem),  ex.getMessage());
   }
  }

}

A preparação e a condição de falha é a mesma do teste anterior. A diferença está na forma que verificamos a ocorrência da exceção.

Veja as duas linhas em destaque:

  • A primeira contém o Assert.fail(); se o código de teste executar essa linha, indica que ele falhou, pois, o teste deveria lançar uma exceção;
  • É no catch() que esperamos que a execução do teste vá. Na segunda linha em destaque, utilizamos o assertEquals para verificar se a mensagem da exceção (ex.getMessage()) é a que esperamos.

Esse código atende muito bem nossas expectativas. Mas não sei o que vocês acham, mas na minha opinião não é muito elegante utilizar try {} catch() para testar a ocorrência de uma exceção e validar sua mensagem.

Pois bem. O JUnit 4 tem uma forma mais elegante de resolver isso.

Como testar exceções em Java #3: @Rule e ExpectedException

O Junit 4.7 introduziu o conceito de Rules.

As Rules, de maneira geral, permitem adicionar comportamentos que serão executados antes e depois de cada método de teste. O JUnit já vem com algumas test rules predefinidas e permite, também, criarmos as nossas próprias Rules. Uma das test rules oferecidas pelo JUnit é a ExpectedException. É ela que vamos usar em nosso próximo exemplo.

Vamos mostrar o código.

public class TransferenciaBancariaServiceTest {

  private TransferenciaBancariaService service = new TransferenciaBancariaService();

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

  @Test
  public void transferenciaImpossivelSaldoInsuficienteDeveLancarExcecao() {
    ContaBancaria origem = new ContaBancaria("CONTA-A", BigDecimal.valueOf(1500l));
    ContaBancaria destino = new ContaBancaria("CONTA-B", BigDecimal.valueOf(500l));

    excecaoEsperada.expect(SaldoInsuficienteException.class);
    excecaoEsperada.expectMessage(String.format(SaldoInsuficienteException.SALDO_INSUFICIENTE_MSG, origem));

    service.transfere(origem, destino, BigDecimal.valueOf(2000l));
  }

}

Uma ExpectedException é uma rule que nos permite verificar se o nosso código lança uma determinada exceção. As linhas em destaque no trecho de código anterior fazem uso dessa rule:

  • As duas primeiras linhas destacadas mostram a declaração da rule. Para isso, utilizamos a anotação @Rule e a classe ExpectedException. Como se pode ver, a varíavel excecaoEsperada é inicializada com o o valor ExpectedException.none(); essa inicialização é para informar que, por padrão, nenhuma exceção é esperada.
  • A próxima linha em destaque – excecaoEsperada.expect() – modifica o comportamento padrão definido anteriormente, informando qual o tipo de exceção esperamos: SaldoInsuficienteException;
  • A última linha destacada nos permite verificar a mensagem da exceção (excecaoEsperada.expectMessage).

Cabe ressaltar algumas coisas sobre os test rules (variáveis anotadas com @Rule) para que elas funcionem adequadamente:

  1. A variável deve ser pública;
  2. A variável não pode ser estática (static);
  3. A variável deve ser um subtipo de TestRule.

Se quiser saber um pouco mais sobre as test rules recomendo a leitura do excelente artigo Testes isolados com jUnit Rules do Rafael Ponte.

Conclusão

Apresentei, neste post, 3 formas diferentes de como testar exceções em Java utilizando o framework JUnit.

Eu, pessoalmente gosto da primeira e terceira formas, pois o código fica mais limpo com as mesmas. Utilizo a primeira quando não preciso verificar a mensagem da exceção. A última, por sua vez, utilizo quando preciso garantir que a mensagem de exceção é a esperada.

Todo o código desse post pode ser encontrado nesse link do meu GitHub.

Abraços e até a próxima.

  • Muito bom! Assim como você eu gosto de trabalhar com a 1a e 3a abordagem; a 1a é interessante quando temos tipos de exceção bem definidas, logo a mensagem costuma ser um mero detalhe.