Introdução: "Por que meu teste falha quando deveria passar?"
\n\nVocê já escreveu um teste unitário que deveria capturar uma exceção, mas ele falhou com um erro vermelho? Familiar? A pergunta "How do you assert that a certain exception is thrown in JUnit tests?" no Stack Overflow acumulou milhares de visualizações e centenas de comentários. E não é por acaso: testar exceções é um daqueles tópicos onde até desenvolvedores experientes às vezes tropeçam.
\n\nEm 2025, quando a qualidade do código se tornou um fator crítico (especialmente em microsserviços e pipelines de IA), a habilidade de verificar exceções corretamente não é apenas "boa prática", mas uma competência básica. Neste artigo, vamos analisar todas as maneiras atuais de assert exceções no JUnit 5, mostrar as armadilhas e fornecer uma checklist de melhores práticas. Vamos lá!
\n\nPor que é importante testar exceções?
\n\nImagine: você escreve o método divide(int a, int b). Se b == 0, o método deve lançar ArithmeticException. Sem um teste para esse comportamento, você corre o risco de que, em um mês, outro desenvolvedor (ou você mesmo) remova acidentalmente a verificação, e a aplicação falhe em produção com um erro incompreensível.
Testes de exceções são o seu seguro. Eles documentam o comportamento esperado do sistema em casos limite. E o JUnit 5 oferece várias ferramentas elegantes para isso.
\n\nMétodo 1: assertThrows — o rei do teste moderno
\n\nA partir do JUnit 5 (e seu pacote org.junit.jupiter.api), o método assertThrows se tornou o padrão de facto. Por quê? Porque é conciso, legível e funcional.
import static org.junit.jupiter.api.Assertions.assertThrows;\n\n@Test\nvoid whenDivideByZero_thenArithmeticException() {\n assertThrows(ArithmeticException.class, \n () -> calculator.divide(10, 0));\n}\n\n\nO que acontece aqui? assertThrows recebe a classe da exceção esperada e uma lambda com o código que deve lançá-la. Se a exceção for lançada, o teste fica verde. Caso contrário, fica vermelho. Simples e claro.
Versão avançada: verificando a mensagem da exceção
\n\nÀs vezes, não basta verificar o tipo da exceção — é preciso garantir que a mensagem contenha o texto correto. assertThrows retorna o objeto da exceção, e você pode verificar suas propriedades:
import static org.junit.jupiter.api.Assertions.*;\n\n@Test\nvoid whenInvalidEmail_thenExceptionWithMessage() {\n IllegalArgumentException exception = assertThrows(\n IllegalArgumentException.class,\n () -> userService.register("invalid-email")\n );\n \n assertTrue(exception.getMessage().contains("Email inválido"));\n}\n\n\nEssa abordagem é especialmente útil ao testar exceções personalizadas com campos (por exemplo, código de erro).
\n\nMétodo 2: O bom e velho expected (legado do JUnit 4)
\n\nSe você trabalha com JUnit 4 ou mantém código legado, provavelmente já viu o atributo expected na anotação @Test:
@Test(expected = ArithmeticException.class)\npublic void whenDivideByZero_thenException() {\n calculator.divide(10, 0);\n}\n\n\nDesvantagem: essa abordagem não permite verificar a mensagem da exceção ou suas propriedades. Além disso, se a exceção for lançada antes do esperado (por exemplo, na linha de inicialização), o teste pode passar falsamente. Em 2025, recomendamos evitar essa abordagem em código novo.
\n\nMétodo 3: Try-catch com flag (para fãs de controle)
\n\nAntes do JUnit 5, muitos usavam a construção com try-catch e uma flag booleana. Parece verboso, mas oferece controle total:
@Test\nvoid whenDivideByZero_thenExceptionCaught() {\n boolean thrown = false;\n try {\n calculator.divide(10, 0);\n } catch (Arithmeti