Asserting Exceptions in JUnit 5

JUnit 5 provides a clean and expressive way to assert that a method throws an expected exception. This is crucial for verifying the error handling behavior of your code. This tutorial will cover how to effectively use assertThrows to write robust unit tests.

Why Assert Exceptions?

When writing unit tests, it’s not enough to verify that a method produces the correct output for valid inputs. You must also ensure that it handles invalid inputs gracefully by throwing appropriate exceptions. Asserting exceptions confirms that your code behaves predictably under error conditions, improving the reliability and maintainability of your application.

Using assertThrows

The assertThrows method is the recommended way to assert that a method throws an exception in JUnit 5. It allows you to specify the expected exception type and a lambda expression containing the code you want to test.

Here’s the basic syntax:

@Test
void testMethodThrowsException() {
    assertThrows(ExpectedExceptionType.class, () -> {
        // Code that is expected to throw the exception
        myObject.doSomething();
    });
}
  • ExpectedExceptionType: The class of the exception you expect to be thrown.
  • () -> { ... }: A lambda expression that encapsulates the code you want to test. This code should, when executed, throw the specified exception.

Example:

Let’s say we have a method that divides two numbers and throws an IllegalArgumentException if the denominator is zero:

public class Calculator {
    public double divide(double numerator, double denominator) {
        if (denominator == 0) {
            throw new IllegalArgumentException("Denominator cannot be zero");
        }
        return numerator / denominator;
    }
}

Here’s how you would test that this method throws the correct exception:

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

public class CalculatorTest {

    @Test
    void testDivideByZeroThrowsException() {
        Calculator calculator = new Calculator();
        assertThrows(IllegalArgumentException.class, () -> {
            calculator.divide(10, 0);
        });
    }
}

In this example, assertThrows asserts that the lambda expression calculator.divide(10, 0) throws an IllegalArgumentException. If the exception is thrown, the test passes. If no exception is thrown, or a different type of exception is thrown, the test fails.

Accessing the Thrown Exception

The assertThrows method returns the thrown exception instance. This allows you to perform further assertions on the exception’s properties, such as its message or any associated data.

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

public class CalculatorTest {

    @Test
    void testDivideByZeroThrowsExceptionWithMessage() {
        Calculator calculator = new Calculator();
        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
            calculator.divide(10, 0);
        });
        assertEquals("Denominator cannot be zero", exception.getMessage());
    }
}

In this example, we capture the IllegalArgumentException instance and then use assertEquals to verify that its message is "Denominator cannot be zero".

Handling Inheritance

If the code under test throws an exception that is a subclass of the expected exception type, assertThrows will still pass. This is because the test only verifies that an exception of the specified type or any of its subclasses is thrown.

If you need to strictly verify that an exception of the exact specified type is thrown, you can use the following approach:

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

public class MyTest {

    @Test
    void testExactExceptionType() {
        // Assume some code throws a MyRuntimeException
        Throwable thrown = assertThrows(Throwable.class, () -> {
            // Code that throws the exception
        });
        assertEquals(MyRuntimeException.class, thrown.getClass());
    }
}

This approach captures the thrown exception as a Throwable and then asserts that its class is exactly the expected exception class using assertEquals.

Choosing the Right Approach

  • Use assertThrows for simple exception type verification.
  • Access the thrown exception instance to verify its message or other properties.
  • For strict exception type verification (excluding subclasses), capture the Throwable and assert its class using assertEquals.

By following these guidelines, you can write clear, concise, and effective unit tests that verify the error handling behavior of your code.

Leave a Reply

Your email address will not be published. Required fields are marked *