This post explores some techniques for asserting exceptions in Java with JUnit.

Table of contents

Using try-catch with fail()

In this approach, the code which is excepted to throw an exception is wrapped in a try-catch block.

Then the fail() method is called immediately after the code that should throw the exception, so that if the exception is not thrown, the test fails. Then assertions can be performed on the exception that has been caught:

import org.junit.Test;

import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;

public class UsingTryCatchWithFail {

    private Foo foo = new Foo();

    @Test
    public void doStuff_shouldThrowException() {

        try {

            // This method is expected to throw a FooException
            foo.doStuff();
            
            // If the exception is not thrown, the test will fail
            fail("Expected exception has not been thrown");

        } catch (FooException e) {

            assertThat(e.getMessage(), is("An exception has occurred"));
            assertThat(e.getCause(), instanceOf(IllegalStateException.class));
        }
    }
}

Using @Test with expected

In this approach, the @Test annotation is used to indicate the expected exception to be thrown in the test:

import org.junit.Test;

public class UsingTestWithExpected {

    private Foo foo = new Foo();

    @Test(expected = FooException.class)
    public void doStuff_shouldThrowException() {

        foo.doStuff();
    }
}

While it’s a simple approach, it lacks the ability of asserting both the message and the cause of the exception that has been thrown. As good exception messages are valuable, assertions on messages should be taken into account.

Also, depending on how the test is written, this approach should be discouraged: As the exception expectation is placed around the whole test method, this might not actually test what is itended to be tested, leading to false positives results, as shown below:

@Test(expected = FooException.class)
public void prepareToDoStuff_shouldSucceed_doStuff_shouldThrowException() {

    // This method may throw a FooException, which may lead to a false positive result
    foo.prepareToDoStuff();

    // This is the method that is supposed to throw the actual FooException being asserted
    foo.doStuff();
}

Using @Rule with ExpectedException

This approach uses the ExpectedException rule to assert an exception and also gives the ability of making assertions on both the message and the cause of the exeption:

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

import static org.hamcrest.core.IsInstanceOf.instanceOf;

public class UsingRuleWithExpectedException {

    private Foo foo = new Foo();

    @Rule
    public final ExpectedException thrown = ExpectedException.none();

    @Test
    public void doStuff_shouldThrowException() {

        thrown.expect(FooException.class);
        thrown.expectMessage("An exception has occurred");
        thrown.expectCause(instanceOf(IllegalStateException.class));

        foo.doStuff();
    }
}

While this approach attempts to fix the caveats of @Test with expected to assert the exception message and cause, it also has issues when it comes to false positives:

@Test
public void prepareToDoStuff_shouldSucceed_doStuff_shouldThrowException() {

    thrown.expect(FooException.class);
    thrown.expectMessage("An exception has occurred");
    thrown.expectCause(instanceOf(IllegalStateException.class));

    // This method may throw a FooException, which may lead to a false positive result
    foo.prepareToDoStuff();

    // This is the method that is supposed to throw the actual FooException being asserted
    foo.doStuff();
}

Finally, if the test follows Behaviour-driven Development (BDD), you’ll find that ExpectedException doesn’t use such writing style.

Using assertThrows from JUnit 5

JUnit 5 aims to solve some problems of JUnit 4 and also takes advantage of Java 8 features, such as lambdas.

When it comes to exceptions, the @Test annotation no longer can be used for indicate the expected exception. As described above, this approach may lead to false positives and doesn’t allow asserting on the exception itself.

As replacement, JUnit 5 introduced the assertThrows() method: It asserts that the execution of the supplied executable throws an exception of the expected type and returns the exception instance, so assertions can be performed on it.

The test will fail if no exception is thrown, or if an exception of a different type is thrown.

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class UsingAssertThrowsFromJUnit5 {

    private Foo foo = new Foo();

    @Test
    @DisplayName("doStuff method should throw exception")
    public void doStuff_shouldThrowException() {

        Throwable thrown = assertThrows(FooException.class, () -> foo.doStuff());

        assertThat(thrown.getMessage(), is("An exception has occurred"));
        assertThat(thrown.getCause(), instanceOf(IllegalStateException.class));
    }
}

Using AssertJ

AssertJ provides a rich API for fluent assertions in Java. It aims to improve the test code readability and make the maintenance of tests easier by providing strongly-typed assertions and intuitive failure messages.

If your tests use at least Java 8, then you can use AssertJ 3.x and leverage on lambdas for asserting exceptions:

import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.catchThrowable;

public class UsingAssertJWithJava8 {

    private Foo foo = new Foo();

    @Test
    public void doStuff_shouldThrowException_1() {

        assertThatExceptionOfType(FooException.class)
                .isThrownBy(() -> foo.doStuff())
                .withMessage("An exception has occurred")
                .withCauseExactlyInstanceOf(IllegalStateException.class);
    }

    @Test
    public void doStuff_shouldThrowException_2() {

        assertThatThrownBy(() -> foo.doStuff())
                .isInstanceOf(FooException.class)
                .hasMessage("An exception has occurred")
                .hasCauseExactlyInstanceOf(IllegalStateException.class);
    }

    @Test
    public void doStuff_shouldThrowException_3() {

        Throwable thrown = catchThrowable(() -> foo.doStuff());

        assertThat(thrown)
                .isInstanceOf(Exception.class)
                .hasMessage("An exception has occurred")
                .hasCauseExactlyInstanceOf(IllegalStateException.class);
    }

    @Test
    public void doStuff_shouldThrowException_4() {

        FooException thrown = catchThrowableOfType(() -> foo.doStuff(), FooException.class);

        assertThat(thrown)
                .hasMessage("An exception has occurred")
                .hasCauseExactlyInstanceOf(IllegalStateException.class);
    }
}

If your tests use Java 7, then you can use the try-catch with fail() approach with AssertJ 2.x and perform fluent assertions on the exception that has been thrown:

import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;
import static org.junit.Assert.fail;

public class UsingAssertJWithJava7 {

    private Foo foo = new Foo();

    @Test
    public void doStuff_shouldThrowException_1() {

        try {

            foo.doStuff();
            fail("Expected exception has not been thrown");

        } catch (FooException e) {

            assertThat(e)
                    .hasMessage("An exception has occurred")
                    .hasCauseExactlyInstanceOf(IllegalStateException.class);
        }
    }

    @Test
    public void doStuff_shouldThrowException_2() {

        try {

            foo.doStuff();
            failBecauseExceptionWasNotThrown(FooException.class);

        } catch (FooException e) {

            assertThat(e)
                    .hasMessage("An exception has occurred")
                    .hasCauseExactlyInstanceOf(IllegalStateException.class);
        }
    }
}

Bottom line and my thoughts

After evaluating the approaches for asserting exceptions described above, I would avoid both @Test with expected and @Rule with ExpectedException approaches, as they may lead to false positive results.

For Java 7, simply stick to the try-catch with fail() approach, even if the test look a bit clumsy.

If you are using at least Java 8 (which I really hope you are), then you can leverage the power of lambdas for assertions. And I strongly encourage you to consider using AssertJ, as it provides a fluent API and the assertions are very close to plain English, which boosts the readability of your tests.