Skip to content

Exception testing

Kevin Cooney edited this page Feb 9, 2020 · 24 revisions

How do you verify that code throws exceptions as expected? Verifying that code completes normally is important, but making sure the code behaves as expected in exceptional situations is vital too. For example:

new ArrayList<Object>().get(0);

This code should throw an IndexOutOfBoundsException. There are multiple ways in JUnit to write a test to verify this behavior.

Using assertThrows Method

The method assertThrows has been added to the Assert class in version 4.13. With this method you can assert that a given function call (specified, for instance, as a lambda expression or method reference) results in a particular type of exception being thrown. In addition it returns the exception that was thrown, so that further assertions can be made (e.g. to verify that the message and cause are correct). Furthermore, you can make assertions on the state of a domain object after the exception has been thrown:

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

import java.util.ArrayList;
import java.util.List;
import org.junit.Test;

@Test
public void testExceptionAndState() {
  List<Object> list = new ArrayList<>();

  IndexOutOfBoundsException thrown = assertThrows(
      IndexOutOfBoundsException.class,
      () -> list.add(1, new Object()));

  // assertions on the thrown exception
  assertEquals("Index: 1, Size: 0", thrown.getMessage());
  // assertions on the state of a domain object after the exception has been thrown
  assertTrue(list.isEmpty());
}

Try/Catch Idiom

If you project is not yet using JUnit 4.13 or your code base does not support lambdas, you can use the try/catch idiom which prevailed in JUnit 3.x:

@Test
public void testExceptionMessage() {
  List<Object> list = new ArrayList<>();
    
  try {
    list.get(0);
    fail("Expected an IndexOutOfBoundsException to be thrown");
  } catch (IndexOutOfBoundsException anIndexOutOfBoundsException) {
    assertThat(anIndexOutOfBoundsException.getMessage(), is("Index: 0, Size: 0"));
  }
}

Be aware that fail() throws an AssertionError, so you cannot use the above idiom to verify that a method call should throw an AssertionError.

Specifying the expected annotation via the @Test annotation.

The @Test annotation has an optional parameter "expected" that takes as values subclasses of Throwable. If we wanted to verify that ArrayList throws the correct exception, we could write:

@Test(expected = IndexOutOfBoundsException.class) 
public void empty() { 
  new ArrayList<Object>().get(0); 
}

The expected parameter should be used with care. The above test will pass if any code in the method throws IndexOutOfBoundsException. Using the method you also cannot test the value of the message in the exception, or the state of a domain object after the exception has been thrown.

For these reasons, the previous approaches are recommended.

ExpectedException Rule

Another way to test exceptions is the ExpectedException rule, but that approach has been deprecated in JUnit 4.13. This rule let you indicate not only what exception you are expecting, but also the exception message you are expecting:

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

@Test
public void shouldTestExceptionMessage() throws IndexOutOfBoundsException {
  List<Object> list = new ArrayList<Object>();
 
  thrown.expect(IndexOutOfBoundsException.class);
  thrown.expectMessage("Index: 0, Size: 0");
  list.get(0); // execution will never get past this line
}

The expectMessage also lets you use Matchers, which gives you a bit more flexibility in your tests. An example:

thrown.expectMessage(CoreMatchers.containsString("Size: 0"));

Moreover, you can use Matchers to inspect the Exception, useful if it has embedded state you wish to verify. For example

import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;

import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

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

public class TestExy {
  @Rule
  public ExpectedException thrown = ExpectedException.none();

  @Test
  public void shouldThrow() {
    TestThing testThing = new TestThing();
    thrown.expect(NotFoundException.class);
    thrown.expectMessage(startsWith("some Message"));
    thrown.expect(hasProperty("response", hasProperty("status", is(404))));
    testThing.chuck();
  }

  private class TestThing {
    public void chuck() {
      Response response = Response.status(Status.NOT_FOUND).entity("Resource not found").build();
      throw new NotFoundException("some Message", response);
    }
  }
}

For an expanded discussion of the ExpectedException rule, see this blog post.

Do note that when the test calls the method under test that throws the exception, no code in the test after the method will execute (because the method under test is throwing the exception). This can lead to confusion, which is one of the reasons why ExpectedException.none() is deprecated.