Java tutorials > Testing and Debugging > Testing > How to test exceptions?

How to test exceptions?

Testing for exceptions in Java is a crucial aspect of ensuring your code's robustness and reliability. It involves verifying that your code throws the expected exceptions under specific conditions. This tutorial explores different techniques for testing exceptions using JUnit, a popular testing framework.

Introduction to Exception Testing

Exception testing is about confirming that a piece of code behaves as expected when things go wrong. Instead of just testing the happy path, you also need to ensure that the code throws the right exception when, for example, it receives invalid input, can't connect to a database, or encounters a file that doesn't exist.

In JUnit, there are a few ways to assert that exceptions are thrown. We'll cover the most common and recommended approaches.

Using assertThrows() (JUnit 5)

JUnit 5's assertThrows() method provides a clean and readable way to test for exceptions. It takes two arguments: the expected exception class and a Executable (a lambda expression or method reference that executes the code you want to test).

The test passes if the code inside the lambda throws an exception of the specified type (or a subclass thereof). If the code doesn't throw any exception, or throws an exception of a different type, the test fails.

Explanation:

  • We import necessary classes from JUnit 5.
  • We create an instance of MyClass.
  • assertThrows(IllegalArgumentException.class, () -> myObject.riskyMethod(-1)) asserts that calling myObject.riskyMethod(-1) throws an IllegalArgumentException.

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

class MyClass {
    public void riskyMethod(int input) {
        if (input < 0) {
            throw new IllegalArgumentException("Input cannot be negative");
        }
        // ... other code ...
    }
}

public class MyClassTest {
    @Test
    void testRiskyMethodThrowsException() {
        MyClass myObject = new MyClass();
        assertThrows(IllegalArgumentException.class, () -> myObject.riskyMethod(-1));
    }
}

Concepts behind the Snippet

The core concept revolves around exception handling and assertion. We are essentially asserting that a particular condition (negative input) results in a specific exception (IllegalArgumentException). The assertThrows method encapsulates the try-catch block typically used for exception handling and assertion, making the test code cleaner and more readable.

Real-Life Use Case Section

Consider a banking application. A method to withdraw money from an account should throw an InsufficientFundsException if the user tries to withdraw more money than they have. Exception testing would verify that this exception is thrown under those conditions, preventing incorrect transactions.

Best Practices

  • Be Specific: Assert for the most specific exception type possible. Avoid catching generic Exception unless absolutely necessary. This makes your tests more precise and easier to understand.
  • Test Exception Message: Often, the exception message contains important information. Use assertThrows to capture the exception and assert on its message.
  • Keep Tests Focused: Each test should focus on testing a single exception condition. Avoid combining multiple exception scenarios in a single test.
  • Arrange, Act, Assert: Follow the standard Arrange, Act, Assert pattern. Arrange the necessary preconditions, Act by calling the method that might throw the exception, and Assert that the expected exception is thrown.

Interview Tip

When discussing exception testing, be prepared to explain the importance of testing exceptional scenarios, the different methods available in JUnit for testing exceptions (assertThrows, rule-based testing (deprecated, but good to know), try-catch block), and the benefits of using specific exception types in assertions.

When to Use Them

Use exception testing whenever a method is designed to throw an exception under certain circumstances. This is especially important for methods that handle user input, interact with external resources (databases, files, APIs), or perform complex calculations where errors are possible.

Memory Footprint

The memory footprint of exception testing is typically negligible. The main overhead comes from creating the objects needed for the test and the temporary creation of the exception object when it's thrown. This overhead is generally insignificant compared to the benefits of thorough testing.

Alternatives

Rule-based Exception Testing (JUnit 4): JUnit 4 used ExpectedException rule. While still functional, it's generally considered less readable and maintainable than assertThrows. The code snippet shows how you can use rule-based exception testing.

Try-Catch Blocks: You can manually use try-catch blocks to test for exceptions, but this approach is more verbose and less elegant than using assertThrows or ExpectedException. It involves wrapping the code that's expected to throw an exception in a try block and catching the expected exception in the catch block. Inside the catch block, you then assert that the caught exception is of the correct type. It is not recommended for new code.

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

public class MyClassTest {

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

    @Test
    public void testRiskyMethodThrowsException() {
        MyClass myObject = new MyClass();
        exceptionRule.expect(IllegalArgumentException.class);
        exceptionRule.expectMessage("Input cannot be negative");
        myObject.riskyMethod(-1);
    }
}

Pros and Cons of assertThrows()

Pros:

  • Readability: Clean and concise syntax.
  • Specificity: Forces you to specify the expected exception type.
  • Modern: Part of JUnit 5 and the recommended approach.

Cons:

  • Requires JUnit 5: Not compatible with older versions of JUnit (4 or earlier).

FAQ

  • How can I assert the message of the exception?

    With assertThrows(), you capture the thrown exception and then assert on its message:

    Exception exception = assertThrows(IllegalArgumentException.class, () -> myObject.riskyMethod(-1));
    assertEquals("Input cannot be negative", exception.getMessage());
  • What happens if the method doesn't throw an exception?

    assertThrows() will fail the test if the code you provide in the lambda expression doesn't throw any exception or if it throws an exception of a different type than the one you specified.

  • Can I test if multiple exceptions are thrown?

    No, assertThrows() is designed to test for a single, specific exception. If you need to test for multiple exceptions (which is generally not recommended), you would typically write separate tests for each exception condition.