One of the most common things you need to test is whether your code throws the
right exceptions under the right conditions. JUnit 5 makes this straightforward
with assertThrows. Let us see how to test exceptions properly.
Basic Exception Testing
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ExceptionTest {
@Test
void shouldThrowException() {
assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("Invalid input");
});
}
@Test
void calculatorShouldThrowOnDivideByZero() {
Calculator calc = new Calculator();
assertThrows(ArithmeticException.class,
() -> calc.divide(10, 0)
);
}
}
Checking the Exception Message
Often you want to verify not just the exception type, but also the message:
@Test
void shouldThrowWithSpecificMessage() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> validateAge(-5)
);
assertEquals("Age must be positive", exception.getMessage());
}
@Test
void shouldThrowWithMessageContaining() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> validateEmail("not-an-email")
);
assertTrue(exception.getMessage().contains("invalid email"));
}
Testing Multiple Exceptions
@Test
void shouldThrowDifferentExceptions() {
// Test case 1: null input
assertThrows(NullPointerException.class,
() -> processString(null)
);
// Test case 2: empty input
assertThrows(IllegalArgumentException.class,
() -> processString("")
);
// Test case 3: valid input (no exception)
assertDoesNotThrow(() -> processString("hello"));
}
@Test
void shouldNotThrowException() {
// assertDoesNotThrow verifies NO exception is thrown
assertDoesNotThrow(() -> {
int result = 2 + 2;
assertEquals(4, result);
});
}
Exception Assertion with Executable
You can capture the exception and do more complex assertions:
@Test
void shouldThrowWithCause() {
Exception exception = assertThrows(RuntimeException.class, () -> {
try {
throw new IOException("File not found");
} catch (IOException e) {
throw new RuntimeException("Failed to process", e);
}
});
assertEquals("Failed to process", exception.getMessage());
assertTrue(exception.getCause() instanceof IOException);
assertEquals("File not found", exception.getCause().getMessage());
}
JUnit 4 vs JUnit 5 Exception Testing
// JUnit 4 approach
@Test(expected = ArithmeticException.class)
public void divideByZero() {
calc.divide(10, 0);
}
// JUnit 5 approach (more powerful)
@Test
void divideByZero() {
ArithmeticException exception = assertThrows(
ArithmeticException.class,
() -> calc.divide(10, 0)
);
assertEquals("Cannot divide by zero", exception.getMessage());
}
The JUnit 5 approach is better because you can inspect the exception object, check the message, verify the cause, and do other assertions. JUnit 4 only let you check the exception type.
Common Patterns
class InputValidatorTest {
@Test
void rejectsNull() {
assertThrows(NullPointerException.class,
() -> validator.validate(null));
}
@Test
void rejectsEmptyString() {
IllegalArgumentException ex = assertThrows(
IllegalArgumentException.class,
() -> validator.validate("")
);
assertEquals("Input cannot be empty", ex.getMessage());
}
@Test
void rejectsTooLong() {
String longString = "a".repeat(1001);
assertThrows(IllegalArgumentException.class,
() -> validator.validate(longString));
}
@Test
void acceptsValidInput() {
assertDoesNotThrow(() -> validator.validate("hello"));
}
}
AssertThrows Returns the Exception
A key detail: assertThrows returns the exception that was thrown. This
lets you chain assertions:
@Test
void detailedExceptionCheck() {
ValidationException ex = assertThrows(
ValidationException.class,
() -> service.process(new Request("", -1))
);
assertEquals(2, ex.getErrors().size());
assertEquals("name", ex.getErrors().get(0).getField());
assertEquals("amount", ex.getErrors().get(1).getField());
}